Skip to content

Commit

Permalink
feat(UI/REST): CycloneDX SBOM Importer & Exporter
Browse files Browse the repository at this point in the history
Signed-off-by: afsahsyeda <[email protected]>
Signed-off-by: akapti <[email protected]>
  • Loading branch information
Abdul Kapti authored and akapti committed Jun 28, 2023
1 parent 32db002 commit 1bf1576
Show file tree
Hide file tree
Showing 48 changed files with 2,522 additions and 239 deletions.
8 changes: 8 additions & 0 deletions backend/src-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,13 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-core-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.package-url</groupId>
<artifactId>packageurl-java</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
* Copyright Siemens Healthineers GmBH, 2023. Part of the SW360 Portal Project.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.sw360.cyclonedx;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.apache.jena.ext.com.google.common.collect.Sets;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.cyclonedx.exception.GeneratorException;
import org.cyclonedx.generators.json.BomJsonGenerator14;
import org.cyclonedx.generators.xml.BomXmlGenerator14;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component.Type;
import org.cyclonedx.model.ExternalReference;
import org.cyclonedx.model.License;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.model.Metadata;
import org.cyclonedx.model.Tool;
import org.eclipse.sw360.datahandler.common.CommonUtils;
import org.eclipse.sw360.datahandler.common.SW360Constants;
import org.eclipse.sw360.datahandler.common.SW360Utils;
import org.eclipse.sw360.datahandler.db.ComponentDatabaseHandler;
import org.eclipse.sw360.datahandler.db.ProjectDatabaseHandler;
import org.eclipse.sw360.datahandler.thrift.CycloneDxComponentType;
import org.eclipse.sw360.datahandler.thrift.RequestStatus;
import org.eclipse.sw360.datahandler.thrift.RequestSummary;
import org.eclipse.sw360.datahandler.thrift.SW360Exception;
import org.eclipse.sw360.datahandler.thrift.ThriftClients;
import org.eclipse.sw360.datahandler.thrift.components.Component;
import org.eclipse.sw360.datahandler.thrift.components.Release;
import org.eclipse.sw360.datahandler.thrift.projects.Project;
import org.eclipse.sw360.datahandler.thrift.projects.ProjectService;
import org.eclipse.sw360.datahandler.thrift.users.User;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

/**
* CycloneDX BOM export implementation.
* Supports both XML and JSON format of CycloneDX SBOM
*
* @author [email protected]
*/
public class CycloneDxBOMExporter {

private static final Logger log = LogManager.getLogger(CycloneDxBOMExporter.class);
private final ProjectDatabaseHandler projectDatabaseHandler;
private final ComponentDatabaseHandler componentDatabaseHandler;
private final User user;

public CycloneDxBOMExporter(ProjectDatabaseHandler projectDatabaseHandler, ComponentDatabaseHandler componentDatabaseHandler, User user) {
this.projectDatabaseHandler = projectDatabaseHandler;
this.componentDatabaseHandler = componentDatabaseHandler;
this.user = user;
}

public RequestSummary exportSbom(String projectId, String bomType, Boolean includeSubProjReleases, User user) {
final RequestSummary summary = new RequestSummary(RequestStatus.SUCCESS);
try {
Project project = projectDatabaseHandler.getProjectById(projectId, user);
Bom bom = new Bom();
Set<String> linkedReleaseIds = Sets.newHashSet(CommonUtils.getNullToEmptyKeyset(project.getReleaseIdToUsage()));

if (!SW360Utils.isUserAtleastDesiredRoleInPrimaryOrSecondaryGroup(user, SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE)) {
log.warn("User does not have permission to export the SBOM: " + user.getEmail());
summary.setRequestStatus(RequestStatus.ACCESS_DENIED);
return summary;
}

if (includeSubProjReleases && project.getLinkedProjectsSize() > 0) {
ProjectService.Iface client = new ThriftClients().makeProjectClient();
linkedReleaseIds.addAll(SW360Utils.getLinkedReleaseIdsOfAllSubProjectsAsFlatList(project, Sets.newHashSet(), Sets.newHashSet(), client, user));
}

if (CommonUtils.isNotEmpty(linkedReleaseIds)) {
List<Release> linkedReleases = componentDatabaseHandler.getReleasesByIds(linkedReleaseIds);
Set<String> componentIds = linkedReleases.stream().map(Release::getComponentId).filter(Objects::nonNull).collect(Collectors.toSet());
List<Component> components = componentDatabaseHandler.getComponentsByIds(componentIds);
List<org.cyclonedx.model.Component> sbomComponents = getCycloneDxComponentsFromSw360Releases(linkedReleases, components);
bom.setComponents(sbomComponents);
} else {
log.warn("Cannot export SBOM for project without linked releases: " + projectId);
summary.setRequestStatus(RequestStatus.FAILED_SANITY_CHECK);
return summary;
}

org.cyclonedx.model.Component metadataComp = getMetadataComponent(project);
Tool tool = getTool();
Metadata metadata = new Metadata();
metadata.setComponent(metadataComp);
metadata.setTimestamp(new Date());
metadata.setTools(Lists.newArrayList(tool));
bom.setMetadata(metadata);

if (SW360Constants.JSON_FILE_EXTENSION.equalsIgnoreCase(bomType)) {
BomJsonGenerator14 jsonBom = new BomJsonGenerator14(bom);
summary.setMessage(jsonBom.toJsonString());
} else {
BomXmlGenerator14 xmlBom = new BomXmlGenerator14(bom);
summary.setMessage(xmlBom.toXmlString());
}
return summary;
} catch (SW360Exception e) {
log.error(String.format("An error occured while fetching project: %s from db, for SBOM export!", projectId), e);
summary.setMessage("An error occured while fetching project from db, for SBOM export: " + e.getMessage());
} catch (GeneratorException e) {
log.error(String.format("An error occured while exporting xml SBOM for project with id: %s", projectId), e);
summary.setMessage("An error occured while exporting xml SBOM for project: " + e.getMessage());
} catch (Exception e) {
log.error("An error occured while exporting SBOm for project: " + projectId, e);
summary.setMessage("An error occured while exporting SBOM for project: " + e.getMessage());
}
summary.setRequestStatus(RequestStatus.FAILURE);
return summary;
}

private static Tool getTool() {
Tool tool = new Tool();
tool.setName(SW360Constants.TOOL_NAME);
tool.setVendor(SW360Constants.TOOL_VENDOR);
tool.setVersion(SW360Utils.getSW360Version());
return tool;
}

private org.cyclonedx.model.Component getMetadataComponent(Project project) {
org.cyclonedx.model.Component component = new org.cyclonedx.model.Component();
component.setAuthor(user.getEmail());
component.setDescription(CommonUtils.nullToEmptyString(project.getDescription()));
component.setName(project.getName());
component.setVersion(CommonUtils.nullToEmptyString(project.getVersion()));
component.setType(Type.APPLICATION);
component.setGroup(CommonUtils.nullToEmptyString(project.getBusinessUnit()));
return component;
}

private List<org.cyclonedx.model.Component> getCycloneDxComponentsFromSw360Releases(List<Release> releases, List<Component> components) {
List<org.cyclonedx.model.Component> comps = Lists.newArrayList();
Map<String, Component> compIdToComponentMap = components.stream()
.map(comp -> new AbstractMap.SimpleEntry<>(comp.getId(), comp))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldVal, newVal) -> newVal));
for (Release release : releases) {
Component sw360Comp = compIdToComponentMap.get(release.getComponentId());
org.cyclonedx.model.Component comp = new org.cyclonedx.model.Component();
comp.setName(release.getName());
comp.setVersion(release.getVersion());
comp.setDescription(CommonUtils.nullToEmptyString(sw360Comp.getDescription()));

// set package URL
Set<String> purlSet = new TreeSet<>();
if (!CommonUtils.isNullOrEmptyMap(release.getExternalIds())) {
if (release.getExternalIds().containsKey(SW360Constants.PACKAGE_URL)) {
purlSet.addAll(getPurlFromSw360Document(release.getExternalIds(), SW360Constants.PACKAGE_URL));
}
if ( release.getExternalIds().containsKey(SW360Constants.PURL_ID)) {
purlSet.addAll(getPurlFromSw360Document(release.getExternalIds(), SW360Constants.PURL_ID));
}
} else if (!CommonUtils.isNullOrEmptyMap(sw360Comp.getExternalIds())) {
if (sw360Comp.getExternalIds().containsKey(SW360Constants.PACKAGE_URL)) {
purlSet.addAll(getPurlFromSw360Document(sw360Comp.getExternalIds(), SW360Constants.PACKAGE_URL));
}
if ( sw360Comp.getExternalIds().containsKey(SW360Constants.PURL_ID)) {
purlSet.addAll(getPurlFromSw360Document(sw360Comp.getExternalIds(), SW360Constants.PURL_ID));
}
}
if (CommonUtils.isNotEmpty(purlSet)) {
comp.setPurl(String.join(", ", purlSet));
}

// set CycloneDx component type
if (sw360Comp.isSetCdxComponentType()) {
comp.setType(getCdxComponentType(sw360Comp.getCdxComponentType()));
}

// set vcs, website and mailing list
List<ExternalReference> extRefs = Lists.newArrayList();
if (null != release.getRepository() && null != release.getRepository().getUrl()) {
ExternalReference extRef = new ExternalReference();
extRef.setType(org.cyclonedx.model.ExternalReference.Type.VCS);
extRef.setUrl(release.getRepository().getUrl());
extRefs.add(extRef);
}
if (CommonUtils.isNotNullEmptyOrWhitespace(sw360Comp.getHomepage())) {
ExternalReference extRef = new ExternalReference();
extRef.setType(org.cyclonedx.model.ExternalReference.Type.WEBSITE);
extRef.setUrl(sw360Comp.getHomepage());
extRefs.add(extRef);
}
if (CommonUtils.isNotNullEmptyOrWhitespace(sw360Comp.getMailinglist())) {
ExternalReference extRef = new ExternalReference();
extRef.setType(org.cyclonedx.model.ExternalReference.Type.MAILING_LIST);
extRef.setUrl(sw360Comp.getMailinglist());
extRefs.add(extRef);
}
if (CommonUtils.isNotNullEmptyOrWhitespace(sw360Comp.getWiki())) {
ExternalReference extRef = new ExternalReference();
extRef.setType(org.cyclonedx.model.ExternalReference.Type.SUPPORT);
extRef.setUrl(sw360Comp.getWiki());
extRefs.add(extRef);
}
if (CommonUtils.isNotEmpty(extRefs)) {
comp.setExternalReferences(extRefs);
}

// set licenses
Set<String> licenses = Sets.newHashSet();
if (CommonUtils.isNotEmpty(release.getMainLicenseIds())) {
licenses.addAll(release.getMainLicenseIds());
}
if (CommonUtils.isNotEmpty(release.getOtherLicenseIds())) {
licenses.addAll(release.getOtherLicenseIds());
}
if (CommonUtils.isNotEmpty(sw360Comp.getMainLicenseIds())) {
licenses.addAll(sw360Comp.getMainLicenseIds());
}
if (CommonUtils.isNotEmpty(licenses)) {
comp.setLicenseChoice(getLicenseFromSw360Document(licenses));
}

comps.add(comp);
}
return comps;
}


private LicenseChoice getLicenseFromSw360Document(Set<String> sw360Licenses) {
LicenseChoice licenseChoice = new LicenseChoice();
List<License> licenses = Lists.newArrayList();
for (String lic : sw360Licenses) {
License license = new License();
license.setId(lic);
licenses.add(license);
}
licenseChoice.setLicenses(licenses);
return licenseChoice;
}

private org.cyclonedx.model.Component.Type getCdxComponentType(CycloneDxComponentType compType) {
switch (compType) {
case APPLICATION:
return org.cyclonedx.model.Component.Type.APPLICATION;
case CONTAINER:
return org.cyclonedx.model.Component.Type.CONTAINER;
case DEVICE:
return org.cyclonedx.model.Component.Type.DEVICE;
case FILE:
return org.cyclonedx.model.Component.Type.FILE;
case FIRMWARE:
return org.cyclonedx.model.Component.Type.FIRMWARE;
case FRAMEWORK:
return org.cyclonedx.model.Component.Type.FRAMEWORK;
case LIBRARY:
return org.cyclonedx.model.Component.Type.LIBRARY;
case OPERATING_SYSTEM:
return org.cyclonedx.model.Component.Type.OPERATING_SYSTEM;
default:
return null;
}
}

@SuppressWarnings("unchecked")
private Set<String> getPurlFromSw360Document(Map<String, String> externalIds, String key) {
String existingPurl = CommonUtils.nullToEmptyMap(externalIds).getOrDefault(key, "");
Set<String> purlSet = Sets.newHashSet();
if (CommonUtils.isNotNullEmptyOrWhitespace(existingPurl)) {
ObjectMapper mapper = new ObjectMapper();
try {
if (existingPurl.equals(SW360Constants.NULL_STRING)) {
purlSet.add(SW360Constants.NULL_STRING);
} else {
purlSet = mapper.readValue(existingPurl, Set.class);
}
} catch (IOException e) {
purlSet.add(existingPurl);
}
}
return purlSet;
}
}
Loading

0 comments on commit 1bf1576

Please sign in to comment.