From 1bf157600e08667c79f83baaf317f55d424b65e3 Mon Sep 17 00:00:00 2001 From: Abdul Kapti Date: Fri, 7 Aug 2020 11:14:09 +0530 Subject: [PATCH] feat(UI/REST): CycloneDX SBOM Importer & Exporter Signed-off-by: afsahsyeda Signed-off-by: akapti --- backend/src-common/pom.xml | 8 + .../sw360/cyclonedx/CycloneDxBOMExporter.java | 294 +++++++++ .../sw360/cyclonedx/CycloneDxBOMImporter.java | 560 ++++++++++++++++++ .../db/ComponentDatabaseHandler.java | 23 +- .../datahandler/db/ComponentRepository.java | 56 +- .../datahandler/db/DatabaseHandlerUtil.java | 30 +- .../db/ProjectDatabaseHandler.java | 56 +- .../datahandler/db/ReleaseRepository.java | 43 +- .../sw360/projects/ProjectHandler.java | 25 +- frontend/sw360-portlet/pom.xml | 4 + .../sw360/portal/common/PortalConstants.java | 8 +- .../sw360/portal/common/PortletUtils.java | 6 + .../portlets/projects/ProjectPortlet.java | 102 +++- .../resources/css/components/_tooltips.scss | 37 ++ .../resources/html/admin/oauthclient/view.jsp | 2 +- .../includes/components/detailOverview.jspf | 2 +- .../includes/components/editBasicInfo.jspf | 40 +- .../includes/components/summary.jspf | 4 + .../includes/releases/detailOverview.jspf | 2 +- .../resources/html/components/view.jsp | 2 +- .../projects/includes/detailOverview.jspf | 192 +++++- .../includes/projects/clearingStatus.jsp | 4 +- .../html/projects/includes/searchProjects.jsp | 4 +- .../META-INF/resources/html/projects/view.jsp | 12 +- .../META-INF/resources/html/search/view.jsp | 2 +- .../html/utils/includes/attachmentsDetail.jsp | 147 ++++- .../html/utils/includes/importBom.jspf | 281 +++++++-- .../html/utils/includes/searchReleases.jsp | 2 +- .../resources/js/utils/includes/clipboard.js | 23 +- .../resources/content/Language.properties | 104 +++- .../resources/content/Language_ja.properties | 99 +++- .../resources/content/Language_vi.properties | 100 +++- .../resources/content/Language_zh.properties | 102 +++- .../sw360/portal/portlets/base.properties | 2 + .../src/main/resources/sw360.properties | 12 + libraries/datahandler/pom.xml | 4 + .../datahandler/common/SW360Constants.java | 16 + .../sw360/datahandler/common/SW360Utils.java | 48 +- .../datahandler/common/ThriftEnumUtils.java | 15 +- .../src/main/thrift/components.thrift | 2 + .../src/main/thrift/projects.thrift | 15 + .../datahandler/src/main/thrift/sw360.thrift | 11 + pom.xml | 12 +- .../src/docs/asciidoc/projects.adoc | 43 +- .../core/JacksonCustomizations.java | 5 +- .../project/ProjectController.java | 81 ++- .../project/Sw360ProjectService.java | 16 +- .../restdocs/ProjectSpecTest.java | 103 +++- 48 files changed, 2522 insertions(+), 239 deletions(-) create mode 100644 backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMExporter.java create mode 100644 backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java diff --git a/backend/src-common/pom.xml b/backend/src-common/pom.xml index 9f8ce85d44..46b95cb588 100644 --- a/backend/src-common/pom.xml +++ b/backend/src-common/pom.xml @@ -55,5 +55,13 @@ org.apache.commons commons-lang3 + + org.cyclonedx + cyclonedx-core-java + + + com.github.package-url + packageurl-java + diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMExporter.java b/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMExporter.java new file mode 100644 index 0000000000..9e5c5f0521 --- /dev/null +++ b/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMExporter.java @@ -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 abdul.kapti@siemens-healthineers.com + */ +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 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 linkedReleases = componentDatabaseHandler.getReleasesByIds(linkedReleaseIds); + Set componentIds = linkedReleases.stream().map(Release::getComponentId).filter(Objects::nonNull).collect(Collectors.toSet()); + List components = componentDatabaseHandler.getComponentsByIds(componentIds); + List 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 getCycloneDxComponentsFromSw360Releases(List releases, List components) { + List comps = Lists.newArrayList(); + Map 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 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 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 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 sw360Licenses) { + LicenseChoice licenseChoice = new LicenseChoice(); + List 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 getPurlFromSw360Document(Map externalIds, String key) { + String existingPurl = CommonUtils.nullToEmptyMap(externalIds).getOrDefault(key, ""); + Set 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; + } +} diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java b/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java new file mode 100644 index 0000000000..2f2288da82 --- /dev/null +++ b/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java @@ -0,0 +1,560 @@ +/* + * 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.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.thrift.TException; +import org.cyclonedx.exception.ParseException; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.ExternalReference.Type; +import org.cyclonedx.model.Metadata; +import org.cyclonedx.parsers.JsonParser; +import org.cyclonedx.parsers.Parser; +import org.cyclonedx.parsers.XmlParser; +import org.eclipse.sw360.commonIO.AttachmentFrontendUtils; +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.common.ThriftEnumUtils; +import org.eclipse.sw360.datahandler.couchdb.AttachmentConnector; +import org.eclipse.sw360.datahandler.db.ComponentDatabaseHandler; +import org.eclipse.sw360.datahandler.db.ProjectDatabaseHandler; +import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestStatus; +import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestSummary; +import org.eclipse.sw360.datahandler.thrift.CycloneDxComponentType; +import org.eclipse.sw360.datahandler.thrift.MainlineState; +import org.eclipse.sw360.datahandler.thrift.ProjectReleaseRelationship; +import org.eclipse.sw360.datahandler.thrift.ReleaseRelationship; +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.Visibility; +import org.eclipse.sw360.datahandler.thrift.attachments.Attachment; +import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentContent; +import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentType; +import org.eclipse.sw360.datahandler.thrift.attachments.CheckStatus; +import org.eclipse.sw360.datahandler.thrift.components.Component; +import org.eclipse.sw360.datahandler.thrift.components.ComponentType; +import org.eclipse.sw360.datahandler.thrift.components.Release; +import org.eclipse.sw360.datahandler.thrift.components.Repository; +import org.eclipse.sw360.datahandler.thrift.components.RepositoryType; +import org.eclipse.sw360.datahandler.thrift.projects.Project; +import org.eclipse.sw360.datahandler.thrift.projects.ProjectType; +import org.eclipse.sw360.datahandler.thrift.users.User; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.github.jsonldjava.shaded.com.google.common.io.Files; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.google.common.net.MediaType; +import com.google.gson.Gson; + +/** + * CycloneDX BOM import implementation. + * Supports both XML and JSON format of CycloneDX SBOM + * + * @author abdul.kapti@siemens-healthineers.com + */ +public class CycloneDxBOMImporter { + private static final Logger log = LogManager.getLogger(CycloneDxBOMImporter.class); + private static final String DOT = "."; + private static final String HYPHEN = "-"; + private static final String JOINER = "||"; + private static final String COLON_REGEX = "[:,\\s]"; + private static final String COMP_CREATION_COUNT_KEY = "compCreationCount"; + private static final String COMP_REUSE_COUNT_KEY = "compReuseCount"; + private static final String REL_CREATION_COUNT_KEY = "relCreationCount"; + private static final String REL_REUSE_COUNT_KEY = "relReuseCount"; + private static final String DUPLICATE_COMPONENT = "dupComp"; + private static final String DUPLICATE_RELEASE = "dupRel"; + private static final String INVALID_RELEASE = "invalidRel"; + private static final String PROJECT_ID = "projectId"; + private static final String PROJECT_NAME = "projectName"; + + private final ProjectDatabaseHandler projectDatabaseHandler; + private final ComponentDatabaseHandler componentDatabaseHandler; + private final User user; + private final AttachmentConnector attachmentConnector; + + public CycloneDxBOMImporter(ProjectDatabaseHandler projectDatabaseHandler, ComponentDatabaseHandler componentDatabaseHandler, + AttachmentConnector attachmentConnector, User user) { + this.projectDatabaseHandler = projectDatabaseHandler; + this.componentDatabaseHandler = componentDatabaseHandler; + this.attachmentConnector = attachmentConnector; + this.user = user; + } + + @SuppressWarnings("unchecked") + public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent attachmentContent, String projectId, User user) { + RequestSummary requestSummary = new RequestSummary(); + Map messageMap = new HashMap<>(); + requestSummary.setRequestStatus(RequestStatus.FAILURE); + String fileExtension = Files.getFileExtension(attachmentContent.getFilename()); + Parser parser; + + if (!SW360Utils.isUserAtleastDesiredRoleInPrimaryOrSecondaryGroup(user, SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE)) { + log.warn("User does not have permission to import the SBOM: " + user.getEmail()); + requestSummary.setMessage(SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE.name()); + requestSummary.setRequestStatus(RequestStatus.ACCESS_DENIED); + return requestSummary; + } + + if (fileExtension.equalsIgnoreCase(SW360Constants.XML_FILE_EXTENSION)) { + parser = new XmlParser(); + } else if (fileExtension.equalsIgnoreCase(SW360Constants.JSON_FILE_EXTENSION)) { + parser = new JsonParser(); + } else { + requestSummary.setMessage(String.format("Invalid file format %s. Only XML & JSON SBOM are supported by CycloneDX!", fileExtension)); + return requestSummary; + } + + try { + // parsing the input stream into CycloneDx org.cyclonedx.model.Bom + Bom bom = parser.parse(IOUtils.toByteArray(inputStream)); + Metadata bomMetadata = bom.getMetadata(); + + // Getting List of org.cyclonedx.model.Component from the Bom + List components = CommonUtils.nullToEmptyList(bom.getComponents()); + + org.cyclonedx.model.Component compMetadata = bomMetadata.getComponent(); + Map> vcsToComponentMap = new HashMap<>(); + + vcsToComponentMap.put("", components); + requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent); + + if (RequestStatus.SUCCESS.equals(requestSummary.getRequestStatus())) { + String jsonMessage = requestSummary.getMessage(); + messageMap = new Gson().fromJson(jsonMessage, Map.class); + String projId = messageMap.get("projectId"); + Project project = projectDatabaseHandler.getProjectById(projId, user); + try { + // link SBOM attachment to Project + if (attachmentContent != null) { + Attachment attachment = makeAttachmentFromContent(attachmentContent); + project.addToAttachments(attachment); + } + RequestStatus updateStatus = projectDatabaseHandler.updateProject(project, user, true); + if (RequestStatus.SUCCESS.equals(updateStatus)) { + log.info("SBOM attachment linked to project successfully: " + project.getId()); + } else { + log.info("failed to link SBOM Import status attachment to project with status: " + updateStatus); + } + messageMap.put("result", requestSummary.getRequestStatus().toString()); + messageMap.put("fileName", attachmentContent.getFilename()); + final StringBuilder fileName = new StringBuilder(attachmentContent.getFilename()) + .append("_ImportStatus_").append(SW360Utils.getCreatedOnTime().replaceAll(COLON_REGEX, HYPHEN)).append(DOT).append(SW360Constants.JSON_FILE_EXTENSION); + final InputStream inStream = IOUtils.toInputStream(convertCollectionToJSONString(messageMap), Charset.defaultCharset()); + final AttachmentContent importResultAttachmentContent = makeAttachmentContent(fileName.toString(), MediaType.JSON_UTF_8.toString()); + Attachment attachment = new AttachmentFrontendUtils().uploadAttachmentContent(importResultAttachmentContent, inStream, user); + attachment.setAttachmentType(AttachmentType.OTHER); + StringBuilder comment = new StringBuilder("Auto Generated: CycloneDX SBOM import result for: '").append(attachmentContent.getFilename()).append("'"); + attachment.setCreatedComment(comment.toString()); + attachment.setSha1(attachmentConnector.getSha1FromAttachmentContentId(importResultAttachmentContent.getId())); + project = projectDatabaseHandler.getProjectById(projId, user); + project.addToAttachments(attachment); + updateStatus = projectDatabaseHandler.updateProject(project, user, true); + if (RequestStatus.SUCCESS.equals(updateStatus)) { + log.info("SBOM Import status attachment linked to project successfully: " + project.getId()); + } else { + log.info("failed to link SBOM Import status attachment to project with status: " + updateStatus); + } + } catch (SW360Exception e) { + log.error("An error occured while updating project from SBOM: " + e.getMessage()); + requestSummary.setMessage("An error occured while updating project during SBOM import, please delete the project and re-import SBOM!"); + return requestSummary; + } + } + + } catch (IOException e) { + log.error("IOException occured while importing CycloneDX SBoM: ", e); + requestSummary.setMessage("IOException occured while importing CycloneDX SBoM: " + e.getMessage()); + } catch (ParseException e) { + log.error("ParseException occured while importing CycloneDX SBoM: ", e); + requestSummary.setMessage("ParseException occured while importing CycloneDX SBoM: " + e.getMessage()); + } catch (SW360Exception e) { + log.error("SW360Exception occured while importing CycloneDX SBoM: ", e); + requestSummary.setMessage("SW360Exception occured while importing CycloneDX SBoM: " + e.getMessage()); + } catch (Exception e) { + log.error("Exception occured while importing CycloneDX SBoM: ", e); + requestSummary.setMessage("An Exception occured while importing CycloneDX SBoM: " + e.getMessage()); + } + return requestSummary; + } + + public RequestSummary importSbomAsProject(org.cyclonedx.model.Component compMetadata, + Map> vcsToComponentMap, String projectId, AttachmentContent attachmentContent) + throws SW360Exception { + final RequestSummary summary = new RequestSummary(); + summary.setRequestStatus(RequestStatus.FAILURE); + + Project project; + AddDocumentRequestSummary projectAddSummary = new AddDocumentRequestSummary(); + AddDocumentRequestStatus addStatus = projectAddSummary.getRequestStatus(); + Map messageMap = new HashMap<>(); + + try { + if (CommonUtils.isNotNullEmptyOrWhitespace(projectId)) { + project = projectDatabaseHandler.getProjectById(projectId, user); + Project sBomProject = createProject(compMetadata); + if (!(sBomProject.getName().equalsIgnoreCase(project.getName()) && sBomProject.getVersion().equalsIgnoreCase(project.getVersion()))) { + log.warn("cannot import SBOM with different metadata information than the current project!"); + summary.setRequestStatus(RequestStatus.FAILED_SANITY_CHECK); + messageMap.put(PROJECT_NAME, SW360Utils.getVersionedName(sBomProject.getName(), sBomProject.getVersion())); + summary.setMessage(convertCollectionToJSONString(messageMap)); + return summary; + } + log.info("reusing existing project: " + projectId); + } else { + // Metadata component is used to created the project + project = createProject(compMetadata); + projectAddSummary = projectDatabaseHandler.addProject(project, user); + addStatus = projectAddSummary.getRequestStatus(); + + if (CommonUtils.isNotNullEmptyOrWhitespace(projectAddSummary.getId())) { + if (AddDocumentRequestStatus.SUCCESS.equals(addStatus)) { + project = projectDatabaseHandler.getProjectById(projectAddSummary.getId(), user); + log.info("project created successfully: " + projectAddSummary.getId()); + } else if (AddDocumentRequestStatus.DUPLICATE.equals(addStatus)) { + log.warn("cannot import SBOM for an existing project from Project List / Home page - " + projectAddSummary.getId()); + summary.setRequestStatus(getRequestStatusFromAddDocRequestStatus(addStatus)); + messageMap.put(PROJECT_ID, projectAddSummary.getId()); + messageMap.put(PROJECT_NAME, SW360Utils.getVersionedName(project.getName(), project.getVersion())); + summary.setMessage(convertCollectionToJSONString(messageMap)); + return summary; + } + } else { + summary.setRequestStatus(getRequestStatusFromAddDocRequestStatus(addStatus)); + summary.setMessage("Invalid Projct metadata present in SBOM or Multiple project with same name and version is already present in SW360!"); + return summary; + } + + } + } catch (SW360Exception e) { + log.error("An error occured while importing project from SBOM: " + e.getMessage()); + summary.setMessage("An error occured while importing project from SBOM!"); + return summary; + } + + messageMap = importAllComponentsAsReleases(vcsToComponentMap, project); + RequestStatus updateStatus = projectDatabaseHandler.updateProject(project, user, true); + if (RequestStatus.SUCCESS.equals(updateStatus)) { + log.info("project updated successfully: " + project.getId()); + } else { + log.info("failed to update project with status: " + updateStatus); + } + summary.setMessage(convertCollectionToJSONString(messageMap)); + summary.setRequestStatus(RequestStatus.SUCCESS); + return summary; + } + + private Map importAllComponentsAsReleases(Map> vcsToComponentMap, Project project) { + + final var countMap = new HashMap(); + final Set duplicateComponents = new HashSet<>(); + final Set duplicateReleases = new HashSet<>(); + final Set invalidReleases = new HashSet<>(); + final Map releaseRelationMap = CommonUtils.isNullOrEmptyMap(project.getReleaseIdToUsage()) ? new HashMap<>() : project.getReleaseIdToUsage(); + countMap.put(COMP_CREATION_COUNT_KEY, 0); countMap.put(COMP_REUSE_COUNT_KEY, 0); + countMap.put(REL_CREATION_COUNT_KEY, 0); countMap.put(REL_REUSE_COUNT_KEY, 0); + int compCreationCount = 0, compReuseCount = 0, relCreationCount = 0, relReuseCount = 0; + + final List components = vcsToComponentMap.get(""); + for (org.cyclonedx.model.Component bomComp : components) { + Component comp = createComponent(bomComp); + if (CommonUtils.isNullEmptyOrWhitespace(comp.getName()) ) { + log.error("component name is not present in SBoM: " + project.getId()); + continue; + } + String relName = ""; + AddDocumentRequestSummary compAddSummary; + try { + compAddSummary = componentDatabaseHandler.addComponent(comp, user.getEmail()); + + if (CommonUtils.isNotNullEmptyOrWhitespace(compAddSummary.getId())) { + comp.setId(compAddSummary.getId()); + if (AddDocumentRequestStatus.SUCCESS.equals(compAddSummary.getRequestStatus())) { + compCreationCount++; + } else { + compReuseCount++; + } + } else { + // in case of more than 1 duplicate found, then continue and show error message in UI. + log.warn("found multiple components: " + comp.getName()); + duplicateComponents.add(comp.getName()); + continue; + } + + Release release = new Release(); + Set licenses = getLicenseFromBomComponent(bomComp); + release = createRelease(bomComp, comp, licenses); + if (CommonUtils.isNullEmptyOrWhitespace(release.getVersion()) ) { + log.error("release version is not present in SBoM for component: " + comp.getName()); + invalidReleases.add(comp.getName()); + continue; + } + relName = SW360Utils.getVersionedName(release.getName(), release.getVersion()); + + try { + AddDocumentRequestSummary relAddSummary = componentDatabaseHandler.addRelease(release, user); + if (CommonUtils.isNotNullEmptyOrWhitespace(relAddSummary.getId())) { + release.setId(relAddSummary.getId()); + if (AddDocumentRequestStatus.SUCCESS.equals(relAddSummary.getRequestStatus())) { + relCreationCount++; + } else { + relReuseCount++; + } + } else { + // in case of more than 1 duplicate found, then continue and show error message in UI. + log.warn("found multiple releases: " + relName); + duplicateReleases.add(relName); + continue; + } + releaseRelationMap.putIfAbsent(release.getId(), getDefaultRelation()); + } catch (SW360Exception e) { + log.error("An error occured while creating/adding release from SBOM: " + e.getMessage()); + continue; + } + + // update components specific fields + comp = componentDatabaseHandler.getComponent(compAddSummary.getId(), user); + if (null != bomComp.getType() && null == comp.getCdxComponentType()) { + comp.setCdxComponentType(getCdxComponentType(bomComp.getType())); + } + StringBuilder description = new StringBuilder(); + if (CommonUtils.isNullEmptyOrWhitespace(comp.getDescription()) && CommonUtils.isNotNullEmptyOrWhitespace(bomComp.getDescription())) { + description.append(bomComp.getDescription().trim()); + } else if (CommonUtils.isNotNullEmptyOrWhitespace(bomComp.getDescription())) { + description.append(" || ").append(bomComp.getDescription().trim()); + } + if (CommonUtils.isNotEmpty(comp.getMainLicenseIds())) { + comp.getMainLicenseIds().addAll(licenses); + } else { + comp.setMainLicenseIds(licenses); + } + for (ExternalReference extRef : CommonUtils.nullToEmptyList(bomComp.getExternalReferences())) { + if (Type.WEBSITE.equals(extRef.getType()) && CommonUtils.isNullEmptyOrWhitespace(comp.getHomepage())) { + comp.setHomepage(CommonUtils.nullToEmptyString(extRef.getUrl())); + } else if (Type.MAILING_LIST.equals(extRef.getType()) && CommonUtils.isNullEmptyOrWhitespace(comp.getMailinglist())) { + comp.setMailinglist(CommonUtils.nullToEmptyString(extRef.getUrl())); + } else if (Type.SUPPORT.equals(extRef.getType()) && CommonUtils.isNullEmptyOrWhitespace(comp.getWiki())) { + comp.setWiki(CommonUtils.nullToEmptyString(extRef.getUrl())); + } + } + comp.setDescription(description.toString()); + RequestStatus updateStatus = componentDatabaseHandler.updateComponent(comp, user, true); + if (RequestStatus.SUCCESS.equals(updateStatus)) { + log.info("updating component successfull: " + comp.getName()); + } + } catch (SW360Exception e) { + log.error("An error occured while creating/adding component from SBOM: " + e.getMessage()); + continue; + } + } + + project.setReleaseIdToUsage(releaseRelationMap); + final Map messageMap = new HashMap<>(); + messageMap.put(DUPLICATE_COMPONENT, String.join(JOINER, duplicateComponents)); + messageMap.put(DUPLICATE_RELEASE, String.join(JOINER, duplicateReleases)); + messageMap.put(INVALID_RELEASE, String.join(JOINER, invalidReleases)); + messageMap.put(PROJECT_ID, project.getId()); + messageMap.put(PROJECT_NAME, SW360Utils.getVersionedName(project.getName(), project.getVersion())); + messageMap.put(COMP_CREATION_COUNT_KEY, String.valueOf(compCreationCount)); + messageMap.put(COMP_REUSE_COUNT_KEY, String.valueOf(compReuseCount)); + messageMap.put(REL_CREATION_COUNT_KEY, String.valueOf(relCreationCount)); + messageMap.put(REL_REUSE_COUNT_KEY, String.valueOf(relReuseCount)); + return messageMap; + } + + + private Set getLicenseFromBomComponent(org.cyclonedx.model.Component comp) { + Set licenses = new HashSet<>(); + if ((null != comp.getLicenseChoice() && CommonUtils.isNotEmpty(comp.getLicenseChoice().getLicenses()))) { + licenses.addAll(comp.getLicenseChoice().getLicenses().stream() + .map(lic -> (null == lic.getId()) ? lic.getName() : lic.getId()) + .filter(lic -> (null != lic && !SW360Constants.NO_ASSERTION.equalsIgnoreCase(lic))) + .collect(Collectors.toSet())); + } + if (null != comp.getEvidence() && null != comp.getEvidence().getLicenseChoice() + && CommonUtils.isNotEmpty(comp.getEvidence().getLicenseChoice().getLicenses())) { + licenses.addAll(comp.getEvidence().getLicenseChoice().getLicenses().stream() + .map(lic -> (null == lic.getId()) ? lic.getName() : lic.getId()) + .filter(lic -> (null != lic && !SW360Constants.NO_ASSERTION.equalsIgnoreCase(lic))) + .collect(Collectors.toSet())); + } + return licenses; + } + + private String convertCollectionToJSONString(Map map) throws SW360Exception { + try { + JsonFactory factory = new JsonFactory(); + StringWriter jsonObjectWriter = new StringWriter(); + JsonGenerator jsonGenerator = factory.createGenerator(jsonObjectWriter); + jsonGenerator.useDefaultPrettyPrinter(); + jsonGenerator.writeStartObject(); + for (Map.Entry entry : map.entrySet()) { + jsonGenerator.writeStringField(entry.getKey(), entry.getValue()); + } + jsonGenerator.writeEndObject(); + jsonGenerator.close(); + return jsonObjectWriter.toString(); + } catch (IOException e) { + throw new SW360Exception("An exception occured while generating JSON info for BOM import! " + e.getMessage()); + } + } + + private RequestStatus getRequestStatusFromAddDocRequestStatus(AddDocumentRequestStatus status) { + switch (status) { + case SUCCESS: + return RequestStatus.SUCCESS; + case DUPLICATE: + return RequestStatus.DUPLICATE; + case NAMINGERROR: + return RequestStatus.NAMINGERROR; + case INVALID_INPUT: + return RequestStatus.INVALID_INPUT; + default: + return RequestStatus.FAILURE; + } + } + + private AttachmentContent makeAttachmentContent(String filename, String contentType) { + AttachmentContent attachment = new AttachmentContent() + .setContentType(contentType) + .setFilename(filename) + .setOnlyRemote(false); + return makeAttachmentContent(attachment); + } + + private AttachmentContent makeAttachmentContent(AttachmentContent content) { + try { + return new AttachmentFrontendUtils().makeAttachmentContent(content); + } catch (TException e) { + throw new RuntimeException(e); + } + } + + private Attachment makeAttachmentFromContent(AttachmentContent attachmentContent) { + Attachment attachment = new Attachment(); + attachment.setAttachmentContentId(attachmentContent.getId()); + attachment.setAttachmentType(AttachmentType.SBOM); + StringBuilder comment = new StringBuilder("Auto Generated: Used for importing CycloneDX SBOM."); + attachment.setCreatedComment(comment.toString()); + attachment.setFilename(attachmentContent.getFilename()); + attachment.setCreatedOn(SW360Utils.getCreatedOn()); + attachment.setCreatedBy(user.getEmail()); + attachment.setCreatedTeam(user.getDepartment()); + attachment.setCheckStatus(CheckStatus.NOTCHECKED); + return attachment; + } + + private Project createProject(org.cyclonedx.model.Component compMetadata) { + return new Project(CommonUtils.nullToEmptyString(compMetadata.getName()).trim()) + .setVersion(CommonUtils.nullToEmptyString(compMetadata.getVersion()).trim()) + .setDescription(CommonUtils.nullToEmptyString(compMetadata.getDescription()).trim()).setType(ThriftEnumUtils.enumToString(ProjectType.PRODUCT)) + .setBusinessUnit(user.getDepartment()).setVisbility(Visibility.EVERYONE); + } + + private Component createComponent(org.cyclonedx.model.Component componentFromBom) { + Component component = new Component(); + component.setName(CommonUtils.nullToEmptyString(componentFromBom.getName()).trim()); + component.setComponentType(ComponentType.OSS); + if (null != componentFromBom.getType()) { + component.setCdxComponentType(getCdxComponentType(componentFromBom.getType())); + } + + for (ExternalReference extRef : CommonUtils.nullToEmptyList(componentFromBom.getExternalReferences())) { + if (Type.WEBSITE.equals(extRef.getType())) { + component.setHomepage(CommonUtils.nullToEmptyString(extRef.getUrl())); + } else if (Type.MAILING_LIST.equals(extRef.getType())) { + component.setMailinglist(CommonUtils.nullToEmptyString(extRef.getUrl())); + } else if (Type.SUPPORT.equals(extRef.getType())) { + component.setWiki(CommonUtils.nullToEmptyString(extRef.getUrl())); + } + } + return component; + } + + private CycloneDxComponentType getCdxComponentType(org.cyclonedx.model.Component.Type compType) { + switch (compType) { + case APPLICATION: + return CycloneDxComponentType.APPLICATION; + case CONTAINER: + return CycloneDxComponentType.CONTAINER; + case DEVICE: + return CycloneDxComponentType.DEVICE; + case FILE: + return CycloneDxComponentType.FILE; + case FIRMWARE: + return CycloneDxComponentType.FIRMWARE; + case FRAMEWORK: + return CycloneDxComponentType.FRAMEWORK; + case LIBRARY: + return CycloneDxComponentType.LIBRARY; + case OPERATING_SYSTEM: + return CycloneDxComponentType.OPERATING_SYSTEM; + default: + return null; + } + } + + private Release createRelease(org.cyclonedx.model.Component componentFromBom, Component component, Set licenses) { + Release release = new Release(component.getName(), CommonUtils.nullToEmptyString(componentFromBom.getVersion()).trim(), component.getId()); + release.setCreatorDepartment(user.getDepartment()); + if (release.isSetMainLicenseIds()) { + release.getMainLicenseIds().addAll(licenses); + } else { + release.setMainLicenseIds(licenses); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(componentFromBom.getPurl())) { + String purl = componentFromBom.getPurl(); + try { + purl = purl.toLowerCase().trim(); + new PackageURL(purl); + Map externalIds = new HashMap<>(); + externalIds.put(SW360Constants.PACKAGE_URL, purl); + release.setExternalIds(externalIds); + } catch (MalformedPackageURLException e) { + log.error("Malformed PURL for component: " + componentFromBom.getName(), e); + } + } + for (ExternalReference extRef : CommonUtils.nullToEmptyList(componentFromBom.getExternalReferences())) { + if (Type.VCS.equals(extRef.getType())) { + String repoUrl = CommonUtils.nullToEmptyString(extRef.getUrl()); + Repository repo = new Repository(repoUrl); + if (repoUrl.toLowerCase().contains("github")) { + repo.setRepositorytype(RepositoryType.GIT); + } else if (repoUrl.toLowerCase().contains("svn")) { + repo.setRepositorytype(RepositoryType.SVN); + } + release.setRepository(repo); + } + } + return release; + } + + private static ProjectReleaseRelationship getDefaultRelation() { + return new ProjectReleaseRelationship(ReleaseRelationship.UNKNOWN, MainlineState.OPEN); + } +} diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentDatabaseHandler.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentDatabaseHandler.java index b5948ebc9a..29c6f216cc 100644 --- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentDatabaseHandler.java +++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentDatabaseHandler.java @@ -67,7 +67,6 @@ import java.io.IOException; import java.io.InputStream; import org.spdx.library.InvalidSPDXAnalysisException; -import java.io.InputStreamReader; import java.net.MalformedURLException; import java.util.*; import java.util.concurrent.TimeUnit; @@ -150,6 +149,7 @@ public class ComponentDatabaseHandler extends AttachmentAwareDatabaseHandler { ClearingInformation._Fields.REQUEST_ID, ClearingInformation._Fields.ADDITIONAL_REQUEST_INFO, ClearingInformation._Fields.EXTERNAL_SUPPLIER_ID, ClearingInformation._Fields.EVALUATED, ClearingInformation._Fields.PROC_START); + public ComponentDatabaseHandler(Supplier httpClient, String dbName, String attachmentDbName, ComponentModerator moderator, ReleaseModerator releaseModerator, ProjectModerator projectModerator) throws MalformedURLException { super(httpClient, dbName, attachmentDbName); DatabaseConnectorCloudant db = new DatabaseConnectorCloudant(httpClient, dbName); @@ -185,6 +185,7 @@ public ComponentDatabaseHandler(Supplier httpClient, String dbNa public ComponentDatabaseHandler(Supplier supplier, String dbName, String attachmentDbName) throws MalformedURLException { this(supplier, dbName, attachmentDbName, new ComponentModerator(), new ReleaseModerator(), new ProjectModerator()); } + public ComponentDatabaseHandler(Supplier supplier, String dbName, String changelogsDbName, String attachmentDbName) throws MalformedURLException { this(supplier, dbName, attachmentDbName, new ComponentModerator(), new ReleaseModerator(), new ProjectModerator()); DatabaseConnectorCloudant db = new DatabaseConnectorCloudant(supplier, changelogsDbName); @@ -488,7 +489,7 @@ public AddDocumentRequestSummary addRelease(Release release, User user) throws S if(isDuplicate(release)) { final AddDocumentRequestSummary addDocumentRequestSummary = new AddDocumentRequestSummary() .setRequestStatus(AddDocumentRequestStatus.DUPLICATE); - List duplicates = releaseRepository.searchByNameAndVersion(release.getName(), release.getVersion()); + List duplicates = releaseRepository.searchByNameAndVersion(release.getName(), release.getVersion(), true); if (duplicates.size() == 1) { duplicates.stream() .map(Release::getId) @@ -573,7 +574,7 @@ private boolean isDuplicate(String releaseName, String releaseVersion) { if (isNullEmptyOrWhitespace(releaseName)) { return false; } - List duplicates = releaseRepository.searchByNameAndVersion(releaseName, releaseVersion); + List duplicates = releaseRepository.searchByNameAndVersion(releaseName, releaseVersion, true); return duplicates.size()>0; } @@ -1061,7 +1062,7 @@ public RequestStatus updateRelease(Release release, User user, Iterable permissions = makePermission(actual, user); @@ -1320,7 +1321,7 @@ public RequestSummary updateReleasesDirectly(Set releases, User user) t return RepositoryUtils.doBulk(prepareReleases(releases), user, releaseRepository); } - public RequestStatus updateReleaseFromAdditionsAndDeletions(Release releaseAdditions, Release releaseDeletions, User user){ + public RequestStatus updateReleaseFromAdditionsAndDeletions(Release releaseAdditions, Release releaseDeletions, User user) { try { Release release = getRelease(releaseAdditions.getId(), user); @@ -1968,6 +1969,16 @@ public List getReleases(Set ids) { return releaseRepository.makeSummary(SummaryType.SHORT, ids); } + // return release directly from db, without making summary. + public List getReleasesByIds(Set ids) { + return CommonUtils.isNullOrEmptyCollection(ids) ? Lists.newArrayList() : releaseRepository.get(ids); + } + + // return components directly from db, without making summary. + public List getComponentsByIds(Set ids) { + return CommonUtils.isNullOrEmptyCollection(ids) ? Lists.newArrayList() : componentRepository.get(ids); + } + public List getAccessibleReleases(Set ids, User user) { return getAccessibleReleaseList(releaseRepository.makeSummary(SummaryType.SHORT, ids), user); } @@ -2196,7 +2207,7 @@ public String getCyclicLinkedReleasePath(Release release, User user) throws TExc } public List searchComponentByNameForExport(String name, boolean caseSensitive) { - return componentRepository.searchByNameForExport(name, caseSensitive); + return componentRepository.searchComponentByName(name, caseSensitive); } public Set getUsingComponents(String releaseId) { diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java index 3381f8dd92..4f3cd3fbb0 100644 --- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java +++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java @@ -37,15 +37,15 @@ */ public class ComponentRepository extends SummaryAwareRepository { private static final String ALL = "function(doc) { if (doc.type == 'component') emit(null, doc._id) }"; - private static final String BYCREATEDON = "function(doc) { if(doc.type == 'component') { emit(doc.createdOn, doc._id) } }"; - private static final String USEDATTACHMENTCONTENTS = "function(doc) { " + + private static final String BY_CREATED_ON = "function(doc) { if(doc.type == 'component') { emit(doc.createdOn, doc._id) } }"; + private static final String USED_ATTACHMENT_CONTENTS = "function(doc) { " + " if(doc.type == 'release' || doc.type == 'component' || doc.type == 'project') {" + " for(var i in doc.attachments){" + " emit(null, doc.attachments[i].attachmentContentId);" + " }" + " }" + "}"; - private static final String MYCOMPONENTS = "function(doc) {" + + private static final String MY_COMPONENTS = "function(doc) {" + " if (doc.type == 'component') {" + " emit(doc.createdBy, doc._id);" + " } " + @@ -57,29 +57,29 @@ public class ComponentRepository extends SummaryAwareRepository { " }" + " }" + "}"; - private static final String BYNAME = "function(doc) {" + + private static final String BY_NAME = "function(doc) {" + " if (doc.type == 'component') {" + " emit(doc.name, doc._id);" + " } " + "}"; - private static final String BYCOMPONENTTYPE = "function(doc) {" + + private static final String BY_COMPONENT_TYPE = "function(doc) {" + " if (doc.type == 'component') {" + " emit(doc.componentType, doc._id);" + " } " + "}"; - private static final String FULLBYNAME = "function(doc) {" + + private static final String FULL_BY_NAME = "function(doc) {" + " if (doc.type == 'component') {" + " emit(doc.name, doc._id);" + " } " + "}"; - private static final String BYLINKINGRELEASE = "function(doc) {" + + private static final String BY_LINKING_RELEASE = "function(doc) {" + " if (doc.type == 'release') {" + " for(var i in doc.releaseIdToRelationship) {" + " emit(i, doc.componentId);" + " }" + " }" + "}"; - private static final String BYFOSSOLOGYID = "function(doc) {\n" + + private static final String BY_FOSSOLOGY_ID = "function(doc) {\n" + " if (doc.type == 'release') {\n" + " if (Array.isArray(doc.externalToolProcesses)) {\n" + " for (var i = 0; i < doc.externalToolProcesses.length; i++) {\n" + @@ -97,7 +97,7 @@ public class ComponentRepository extends SummaryAwareRepository { " }\n" + " }\n" + "}"; - private static final String BYEXTERNALIDS = "function(doc) {" + + private static final String BY_EXTERNAL_IDS = "function(doc) {" + " if (doc.type == 'component') {" + " for (var externalId in doc.externalIds) {" + " try {" + @@ -115,19 +115,19 @@ public class ComponentRepository extends SummaryAwareRepository { " }" + " }" + "}"; - private static final String BYDEFAULTVENDORID = "function(doc) {" + + private static final String BY_DEFAULT_VENDOR_ID = "function(doc) {" + " if (doc.type == 'component') {" + " emit( doc.defaultVendorId , doc._id);" + " }" + "}"; - private static final String BYNAMELOWERCASE = "function(doc) {" + + private static final String BY_NAME_LOWERCASE = "function(doc) {" + " if (doc.type == 'component') {" + - " emit(doc.name.toLowerCase(), doc._id);" + + " emit(doc.name.toLowerCase().trim(), doc._id);" + " } " + "}"; - private static final String BYMAINLICENSE = "function(doc) {" + + private static final String BY_MAIN_LICENSE = "function(doc) {" + " if (doc.type == 'component') {" + " if(doc.mainLicenseIds) {" + " emit(doc.mainLicenseIds.join(), doc._id);" + @@ -137,7 +137,7 @@ public class ComponentRepository extends SummaryAwareRepository { " }" + "}"; - private static final String BYVENDOR = "function(doc) {" + + private static final String BY_VENDOR = "function(doc) {" + " if (doc.type == 'component') {" + " if(doc.vendorNames) {" + " emit(doc.vendorNames.join(), doc._id);" + @@ -151,20 +151,20 @@ public ComponentRepository(DatabaseConnectorCloudant db, ReleaseRepository relea super(Component.class, db, new ComponentSummary(releaseRepository, vendorRepository)); Map views = new HashMap(); views.put("all", createMapReduce(ALL, null)); - views.put("byCreatedOn", createMapReduce(BYCREATEDON, null)); - views.put("usedAttachmentContents", createMapReduce(USEDATTACHMENTCONTENTS, null)); - views.put("mycomponents", createMapReduce(MYCOMPONENTS, null)); + views.put("byCreatedOn", createMapReduce(BY_CREATED_ON, null)); + views.put("usedAttachmentContents", createMapReduce(USED_ATTACHMENT_CONTENTS, null)); + views.put("mycomponents", createMapReduce(MY_COMPONENTS, null)); views.put("subscribers", createMapReduce(SUBSCRIBERS, null)); - views.put("byname", createMapReduce(BYNAME, null)); - views.put("bycomponenttype", createMapReduce(BYCOMPONENTTYPE, null)); - views.put("fullbyname", createMapReduce(FULLBYNAME, null)); - views.put("byLinkingRelease", createMapReduce(BYLINKINGRELEASE, null)); - views.put("byFossologyId", createMapReduce(BYFOSSOLOGYID, null)); - views.put("byExternalIds", createMapReduce(BYEXTERNALIDS, null)); - views.put("byDefaultVendorId", createMapReduce(BYDEFAULTVENDORID, null)); - views.put("bynamelowercase", createMapReduce(BYNAMELOWERCASE, null)); - views.put("bymainlicense", createMapReduce(BYMAINLICENSE, null)); - views.put("byvendor", createMapReduce(BYVENDOR, null)); + views.put("byname", createMapReduce(BY_NAME, null)); + views.put("bycomponenttype", createMapReduce(BY_COMPONENT_TYPE, null)); + views.put("fullbyname", createMapReduce(FULL_BY_NAME, null)); + views.put("byLinkingRelease", createMapReduce(BY_LINKING_RELEASE, null)); + views.put("byFossologyId", createMapReduce(BY_FOSSOLOGY_ID, null)); + views.put("byExternalIds", createMapReduce(BY_EXTERNAL_IDS, null)); + views.put("byDefaultVendorId", createMapReduce(BY_DEFAULT_VENDOR_ID, null)); + views.put("bynamelowercase", createMapReduce(BY_NAME_LOWERCASE, null)); + views.put("bymainlicense", createMapReduce(BY_MAIN_LICENSE, null)); + views.put("byvendor", createMapReduce(BY_VENDOR, null)); initStandardDesignDocument(views, db); } @@ -215,7 +215,7 @@ public Set getComponentIdsByName(String name, boolean caseInsenstive) { return queryForIdsAsValue("byname", name); } - public List searchByNameForExport(String name, boolean caseSensitive) { + public List searchComponentByName(String name, boolean caseSensitive) { Set componentIds; if (caseSensitive) { componentIds = queryForIdsAsValueByPrefix("fullbyname", name); diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java index b9ae179cc3..904349fdf6 100644 --- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java +++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java @@ -400,9 +400,9 @@ public static void trimStringFields(T obj, List listOfStrFields) { changeLog.setDocumentType(newProjVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE); } else if (newDocVersion instanceof ObligationList) { - ObligationList newProjVer = (ObligationList) newDocVersion; - changeLog.setDocumentId(newProjVer.getId()); - changeLog.setDocumentType(newProjVer.getType()); + ObligationList newOblListVer = (ObligationList) newDocVersion; + changeLog.setDocumentId(newOblListVer.getId()); + changeLog.setDocumentType(newOblListVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE); } else if (newDocVersion instanceof AttachmentContent) { AttachmentContent newAttachmentContentVer = (AttachmentContent) newDocVersion; @@ -412,19 +412,19 @@ public static void trimStringFields(T obj, List listOfStrFields) { info.put(AttachmentContent._Fields.FILENAME.name(), newAttachmentContentVer.getFilename()); info.put(AttachmentContent._Fields.CONTENT_TYPE.name(), newAttachmentContentVer.getContentType()); } else if (newDocVersion instanceof Component) { - Component newProjVer = (Component) newDocVersion; - changeLog.setDocumentId(newProjVer.getId()); - changeLog.setDocumentType(newProjVer.getType()); + Component newCompVer = (Component) newDocVersion; + changeLog.setDocumentId(newCompVer.getId()); + changeLog.setDocumentType(newCompVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE); } else if (newDocVersion instanceof Release) { - Release newProjVer = (Release) newDocVersion; - changeLog.setDocumentId(newProjVer.getId()); - changeLog.setDocumentType(newProjVer.getType()); + Release newRelVer = (Release) newDocVersion; + changeLog.setDocumentId(newRelVer.getId()); + changeLog.setDocumentType(newRelVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE); } else if (newDocVersion instanceof ModerationRequest) { - ModerationRequest newProjVer = (ModerationRequest) newDocVersion; - changeLog.setDocumentId(newProjVer.getId()); - changeLog.setDocumentType(newProjVer.getType()); + ModerationRequest newModReqVer = (ModerationRequest) newDocVersion; + changeLog.setDocumentId(newModReqVer.getId()); + changeLog.setDocumentType(newModReqVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE); } else if (newDocVersion instanceof SPDXDocument) { SPDXDocument newProjVer = (SPDXDocument) newDocVersion; @@ -442,9 +442,9 @@ public static void trimStringFields(T obj, List listOfStrFields) { changeLog.setDocumentType(newProjVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_SPDX); } else if (newDocVersion instanceof Obligation) { - Obligation newProjVer = (Obligation) newDocVersion; - changeLog.setDocumentId(newProjVer.getId()); - changeLog.setDocumentType(newProjVer.getType()); + Obligation newOblVer = (Obligation) newDocVersion; + changeLog.setDocumentId(newOblVer.getId()); + changeLog.setDocumentType(newOblVer.getType()); changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE); } diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java index 650bdfaa80..e07d382dcb 100644 --- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java +++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java @@ -17,11 +17,14 @@ import com.google.common.collect.*; import com.google.gson.JsonArray; import com.google.gson.JsonObject; + +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.thrift.TException; - import org.eclipse.sw360.common.utils.BackendUtils; import org.eclipse.sw360.components.summary.SummaryType; +import org.eclipse.sw360.cyclonedx.CycloneDxBOMExporter; +import org.eclipse.sw360.cyclonedx.CycloneDxBOMImporter; import org.eclipse.sw360.datahandler.businessrules.ReleaseClearingStateSummaryComputer; import org.eclipse.sw360.datahandler.cloudantclient.DatabaseConnectorCloudant; import org.eclipse.sw360.datahandler.common.*; @@ -54,6 +57,7 @@ import java.io.*; import java.net.MalformedURLException; +import java.nio.charset.Charset; import java.time.Instant; import java.util.*; import java.util.concurrent.TimeUnit; @@ -1099,7 +1103,7 @@ public List getReleaseClearingStatusesWithAccessibili ReleaseClearingStatusData releaseClearingStatusData = new ReleaseClearingStatusData(release) .setProjectNames(joinStrings(projectNames)) .setMainlineStates(joinStrings(mainlineStates)) - .setComponentType(componentsById.get(release.getComponentId()).getComponentType()); + .setComponentType(componentsById.get(release.getComponentId()).getComponentType()); boolean isAccessible = componentDatabaseHandler.isReleaseActionAllowed(release, user, RequestedAction.READ); releaseClearingStatusData.setAccessible(isAccessible); @@ -1699,6 +1703,48 @@ public RequestSummary importBomFromAttachmentContent(User user, String attachmen } } + public RequestSummary importCycloneDxFromAttachmentContent(User user, String attachmentContentId, String projectId) throws SW360Exception { + final AttachmentContent attachmentContent = attachmentConnector.getAttachmentContent(attachmentContentId); + final Duration timeout = Duration.durationOf(30, TimeUnit.SECONDS); + try { + final AttachmentStreamConnector attachmentStreamConnector = new AttachmentStreamConnector(timeout); + try (final InputStream inputStream = attachmentStreamConnector + .unsafeGetAttachmentStream(attachmentContent)) { + final CycloneDxBOMImporter cycloneDxBOMImporter = new CycloneDxBOMImporter(this, + componentDatabaseHandler, attachmentConnector, user); + return cycloneDxBOMImporter.importFromBOM(inputStream, attachmentContent, projectId, user); + } + } catch (IOException e) { + log.error("Error while importing / parsing CycloneDX SBOM! ", e); + throw new SW360Exception(e.getMessage()); + } + } + + public RequestSummary exportCycloneDxSbom(String projectId, String bomType, Boolean includeSubProjReleases, User user) throws SW360Exception { + try { + final CycloneDxBOMExporter cycloneDxBOMExporter = new CycloneDxBOMExporter(this, componentDatabaseHandler, user); + return cycloneDxBOMExporter.exportSbom(projectId, bomType, includeSubProjReleases, user); + } catch (Exception e) { + log.error("Error while exporting CycloneDX SBOM! ", e); + throw new SW360Exception(e.getMessage()); + } + } + + public String getSbomImportInfoFromAttachmentAsString(String attachmentContentId) throws SW360Exception { + final AttachmentContent attachmentContent = attachmentConnector.getAttachmentContent(attachmentContentId); + final Duration timeout = Duration.durationOf(30, TimeUnit.SECONDS); + try { + final AttachmentStreamConnector attachmentStreamConnector = new AttachmentStreamConnector(timeout); + try (final InputStream inputStream = attachmentStreamConnector + .unsafeGetAttachmentStream(attachmentContent)) { + return IOUtils.toString(inputStream, Charset.defaultCharset()); + } + } catch (IOException e) { + log.error("Error while getting sbom import info from attachment! ", e); + throw new SW360Exception(e.getMessage()); + } + } + private void removeLeadingTrailingWhitespace(Project project) { DatabaseHandlerUtil.trimStringFields(project, listOfStringFieldsInProjToTrim); @@ -1781,7 +1827,7 @@ private void flattenClearingStatusForReleases(Map releaseIdToRelationship = rel.getReleaseIdToRelationship(); releaseOrigin.put(releaseId, SW360Utils.printName(rel)); @@ -1811,7 +1857,7 @@ private void flattenlinkedReleaseOfRelease(Map rele if (releaseOrigin.containsKey(releaseId)) return; Release rel = componentDatabaseHandler.getRelease(releaseId, user); - + if (!isInaccessibleLinkMasked || componentDatabaseHandler.isReleaseActionAllowed(rel, user, RequestedAction.READ)) { Map subReleaseIdToRelationship = rel.getReleaseIdToRelationship(); releaseOrigin.put(releaseId, SW360Utils.printName(rel)); @@ -1873,7 +1919,7 @@ private Map createReleaseCSRow(String relation, String projectMa clearingStatusList.add(row); return row; } - + private Map createInaccessibleReleaseCSRow(List> clearingStatusList) throws SW360Exception { Map row = new HashMap<>(); row.put("id", ""); diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseRepository.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseRepository.java index 8b7064fbfc..aee6803f4b 100644 --- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseRepository.java +++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseRepository.java @@ -43,8 +43,8 @@ public class ReleaseRepository extends SummaryAwareRepository { private static final String ALL = "function(doc) { if (doc.type == 'release') emit(null, doc._id) }"; - private static final String BYNAME = "function(doc) { if(doc.type == 'release') { emit(doc.name, doc._id) } }"; - private static final String BYCREATEDON = "function(doc) { if(doc.type == 'release') { emit(doc.createdOn, doc._id) } }"; + private static final String BY_NAME = "function(doc) { if(doc.type == 'release') { emit(doc.name, doc._id) } }"; + private static final String BY_CREATED_ON = "function(doc) { if(doc.type == 'release') { emit(doc.createdOn, doc._id) } }"; private static final String SUBSCRIBERS = "function(doc) {" + " if (doc.type == 'release'){" + " for(var i in doc.subscribers) {" + @@ -52,38 +52,38 @@ public class ReleaseRepository extends SummaryAwareRepository { " }" + " }" + "}"; - private static final String USEDINRELEASERELATION = "function(doc) {" + + private static final String USED_IN_RELEASE_RELATION = "function(doc) {" + " if(doc.type == 'release') {" + " for(var id in doc.releaseIdToRelationship) {" + " emit(id, doc._id);" + " }" + " }" + "}"; - private static final String RELEASEBYVENDORID = "function(doc) {" + + private static final String RELEASE_BY_VENDOR_ID = "function(doc) {" + " if (doc.type == 'release'){" + " emit(doc.vendorId, doc._id);" + " }" + "}"; - private static final String RELEASESBYCOMPONENTID = "function(doc) {" + + private static final String RELEASES_BY_COMPONENT_ID = "function(doc) {" + " if (doc.type == 'release'){" + " emit(doc.componentId, doc._id);" + " }" + "}"; - private static final String RELEASEIDSBYVENDORID = "function(doc) {" + + private static final String RELEASE_IDS_BY_VENDOR_ID = "function(doc) {" + " if (doc.type == 'release'){" + " emit(doc.vendorId, doc._id);" + " }" + "}"; - private static final String RELEASEIDSBYLICENSEID = "function(doc) {" + + private static final String RELEASE_IDS_BY_LICENSE_ID = "function(doc) {" + " if (doc.type == 'release'){" + " for(var i in doc.mainLicenseIds) {" + " emit(doc.mainLicenseIds[i], doc._id);" + " }" + " }" + "}"; - private static final String BYEXTERNALIDS = "function(doc) {" + + private static final String BY_EXTERNAL_IDS = "function(doc) {" + " if (doc.type == 'release') {" + " for (var externalId in doc.externalIds) {" + " try {" + @@ -171,19 +171,19 @@ public ReleaseRepository(DatabaseConnectorCloudant db, VendorRepository vendorRe super(Release.class, db, new ReleaseSummary(vendorRepository)); Map views = new HashMap(); views.put("all", createMapReduce(ALL, null)); - views.put("byname", createMapReduce(BYNAME, null)); - views.put("byCreatedOn", createMapReduce(BYCREATEDON, null)); + views.put("byname", createMapReduce(BY_NAME, null)); + views.put("byCreatedOn", createMapReduce(BY_CREATED_ON, null)); views.put("subscribers", createMapReduce(SUBSCRIBERS, null)); - views.put("usedInReleaseRelation", createMapReduce(USEDINRELEASERELATION, null)); - views.put("releaseByVendorId", createMapReduce(RELEASEBYVENDORID, null)); - views.put("releasesByComponentId", createMapReduce(RELEASESBYCOMPONENTID, null)); - views.put("releaseIdsByLicenseId", createMapReduce(RELEASEIDSBYLICENSEID, null)); - views.put("byExternalIds", createMapReduce(BYEXTERNALIDS, null)); + views.put("usedInReleaseRelation", createMapReduce(USED_IN_RELEASE_RELATION, null)); + views.put("releaseByVendorId", createMapReduce(RELEASE_BY_VENDOR_ID, null)); + views.put("releasesByComponentId", createMapReduce(RELEASES_BY_COMPONENT_ID, null)); + views.put("releaseIdsByLicenseId", createMapReduce(RELEASE_IDS_BY_LICENSE_ID, null)); + views.put("byExternalIds", createMapReduce(BY_EXTERNAL_IDS, null)); views.put("releaseByCpeId", createMapReduce(BY_LOWERCASE_RELEASE_CPE_VIEW, null)); views.put("releaseBySvmId", createMapReduce(RELEASES_BY_SVM_ID_RELEASE_VIEW, null)); views.put("releaseByName", createMapReduce(BY_LOWERCASE_RELEASE_NAME_VIEW, null)); views.put("releaseByVersion", createMapReduce(BY_LOWERCASE_RELEASE_VERSION_VIEW, null)); - views.put("releaseIdsByVendorId", createMapReduce(RELEASEIDSBYVENDORID, null)); + views.put("releaseIdsByVendorId", createMapReduce(RELEASE_IDS_BY_VENDOR_ID, null)); views.put("byStatus", createMapReduce(BY_STATUS_VIEW, null)); views.put("byECCAssessorContactPerson", createMapReduce(BY_ASSESSOR_CONTACT_PERSON_VIEW, null)); views.put("byECCAssessorGroup", createMapReduce(BY_ASSESSOR_DEPARTMENT_VIEW, null)); @@ -196,10 +196,15 @@ public List searchByNamePrefix(String name) { return makeSummary(SummaryType.SHORT, queryForIdsByPrefix("byname", name)); } - public List searchByNameAndVersion(String name, String version){ - List releasesMatchingName = new ArrayList(getFullDocsById(queryForIdsAsValue("byname", name))); + public List searchByNameAndVersion(String name, String version, boolean caseInsenstive){ + List releasesMatchingName; + if (caseInsenstive) { + releasesMatchingName = new ArrayList(queryView("releaseByName", name.toLowerCase())); + } else { + releasesMatchingName = new ArrayList(queryView("byname", name)); + } List releasesMatchingNameAndVersion = releasesMatchingName.stream() - .filter(r -> isNullOrEmpty(version) ? isNullOrEmpty(r.getVersion()) : version.equals(r.getVersion())) + .filter(r -> isNullOrEmpty(version) ? isNullOrEmpty(r.getVersion()) : version.equalsIgnoreCase(r.getVersion())) .collect(Collectors.toList()); return releasesMatchingNameAndVersion; } diff --git a/backend/src/src-projects/src/main/java/org/eclipse/sw360/projects/ProjectHandler.java b/backend/src/src-projects/src/main/java/org/eclipse/sw360/projects/ProjectHandler.java index 4ef086eecd..1c3499fd9e 100644 --- a/backend/src/src-projects/src/main/java/org/eclipse/sw360/projects/ProjectHandler.java +++ b/backend/src/src-projects/src/main/java/org/eclipse/sw360/projects/ProjectHandler.java @@ -20,8 +20,8 @@ import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestSummary; import org.eclipse.sw360.datahandler.thrift.PaginationData; import org.eclipse.sw360.datahandler.thrift.RequestStatus; -import org.eclipse.sw360.datahandler.thrift.SW360Exception; import org.eclipse.sw360.datahandler.thrift.RequestSummary; +import org.eclipse.sw360.datahandler.thrift.SW360Exception; import org.eclipse.sw360.datahandler.thrift.attachments.Attachment; import org.eclipse.sw360.datahandler.thrift.components.ReleaseClearingStatusData; import org.eclipse.sw360.datahandler.thrift.projects.ClearingRequest; @@ -30,7 +30,6 @@ import org.eclipse.sw360.datahandler.thrift.projects.ProjectData; import org.eclipse.sw360.datahandler.thrift.projects.ProjectLink; import org.eclipse.sw360.datahandler.thrift.projects.ObligationList; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectRelationship; import org.eclipse.sw360.datahandler.thrift.projects.ProjectService; import org.eclipse.sw360.datahandler.thrift.projects.UsedReleaseRelations; import org.eclipse.sw360.datahandler.thrift.users.User; @@ -39,11 +38,11 @@ import com.cloudant.client.api.CloudantClient; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.*; import java.util.function.Supplier; import static org.eclipse.sw360.datahandler.common.SW360Assert.*; -import java.nio.ByteBuffer; /** * Implementation of the Thrift service @@ -237,6 +236,26 @@ public RequestSummary importBomFromAttachmentContent(User user, String attachmen return handler.importBomFromAttachmentContent(user, attachmentContentId); } + @Override + public RequestSummary importCycloneDxFromAttachmentContent(User user, String attachmentContentId, String projectId) throws SW360Exception { + assertId(attachmentContentId); + assertUser(user); + return handler.importCycloneDxFromAttachmentContent(user, attachmentContentId, projectId); + } + + @Override + public RequestSummary exportCycloneDxSbom(String projectId, String bomType, boolean includeSubProjReleases, User user) throws SW360Exception { + assertId(projectId); + assertUser(user); + return handler.exportCycloneDxSbom(projectId, bomType, includeSubProjReleases, user); + } + + @Override + public String getSbomImportInfoFromAttachmentAsString(String attachmentContentId) throws SW360Exception { + assertId(attachmentContentId); + return handler.getSbomImportInfoFromAttachmentAsString(attachmentContentId); + } + //////////////////////////// // ADD INDIVIDUAL OBJECTS // //////////////////////////// diff --git a/frontend/sw360-portlet/pom.xml b/frontend/sw360-portlet/pom.xml index 47efd557f4..61459ff90b 100644 --- a/frontend/sw360-portlet/pom.xml +++ b/frontend/sw360-portlet/pom.xml @@ -216,6 +216,10 @@ jstl-api 1.2 + + com.github.package-url + packageurl-java + com.google.guava guava diff --git a/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortalConstants.java b/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortalConstants.java index f0065adce7..19f6604969 100644 --- a/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortalConstants.java +++ b/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortalConstants.java @@ -26,6 +26,7 @@ * @author stefan.jaeger@evosoft.com * @author alex.borodin@evosoft.com */ + public class PortalConstants { public static final String PROPERTIES_FILE_PATH = "/sw360.properties"; @@ -277,6 +278,7 @@ public class PortalConstants { public static final String RELEASE_SEARCH_BY_VENDOR = "releaseSearchByVendor"; public static final String OBLIGATION_ELEMENT_SEARCH = "obligationElementSearch"; public static final String OBLIGATION_ELEMENT_ID = "obligationElementId"; + public static final String LOAD_SBOM_IMPORT_INFO = "loadSbomImportInfo"; public static final String RELEASE_LIST_FROM_LINKED_PROJECTS = "releaseListFromLinkedProjects"; public static final String STATE; @@ -552,12 +554,16 @@ public class PortalConstants { public static final String PARENT_BRANCH_ID = "parent_branch_id"; public static final String PARENT_SCOPE_GROUP_ID = "parentScopeGroupId"; - // bom import + // bom import / export public static final String PREPARE_IMPORT_BOM = "prepareImportBom"; public static final String IMPORT_BOM = "importBom"; public static final String IMPORT_BOM_AS_NEW = "importBomAsNew"; public static final String NEW_RELEASE_VERSION = "newReleaseVersion"; public static final String RDF_FILE_PATH = "rdfFilePath"; + public static final String BOM_TYPE = "bomType"; + public static final String EXPORT_SBOM = "exportSbom"; + public static final String SBOM_FROMAT = "sbomFormat"; + public static final String IS_SBOM_IMPORT_EXPORT_ACCESS_USER = "isSbomImportExportAccessUser"; // project actions public static final String VIEW_LINKED_PROJECTS = "view_linked_projects"; diff --git a/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortletUtils.java b/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortletUtils.java index 7c99ffd0f2..5965790063 100644 --- a/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortletUtils.java +++ b/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/common/PortletUtils.java @@ -150,6 +150,10 @@ public static ObligationType getObligationTypeFromString(String enumNumber) { return ObligationType.findByValue(parseInt(enumNumber)); } + public static CycloneDxComponentType getCycloneDxComponentTypeFromString(String enumNumber) { + return CycloneDxComponentType.findByValue(parseInt(enumNumber)); + } + public static > void setFieldValue(PortletRequest request, T instance, U field, FieldMetaData fieldMetaData, String prefix) { String value = request.getParameter(prefix + field.toString()); @@ -205,6 +209,8 @@ else if (field == Obligation._Fields.OBLIGATION_LEVEL) return getObligationLevelFromString(value); else if (field == Obligation._Fields.OBLIGATION_TYPE) return getObligationTypeFromString(value); + else if (field == Component._Fields.CDX_COMPONENT_TYPE) + return getCycloneDxComponentTypeFromString(value); else { LOGGER.error("Missing case in enumFromString, unknown field was " + field.toString()); return null; diff --git a/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/portlets/projects/ProjectPortlet.java b/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/portlets/projects/ProjectPortlet.java index 68ecb78252..c0a70119b7 100644 --- a/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/portlets/projects/ProjectPortlet.java +++ b/frontend/sw360-portlet/src/main/java/org/eclipse/sw360/portal/portlets/projects/ProjectPortlet.java @@ -25,6 +25,7 @@ import com.liferay.portal.kernel.servlet.SessionMessages; import com.liferay.portal.kernel.servlet.SessionErrors; import com.liferay.portal.kernel.theme.ThemeDisplay; +import com.liferay.portal.kernel.util.ContentTypes; import com.liferay.portal.kernel.util.PortalUtil; import com.liferay.portal.kernel.util.WebKeys; import com.liferay.portal.kernel.util.ResourceBundleUtil; @@ -238,6 +239,8 @@ public void serveResource(ResourceRequest request, ResourceResponse response) th removeOrphanObligation(request, response); } else if (PortalConstants.IMPORT_BOM.equals(action)) { importBom(request, response); + } else if (PortalConstants.EXPORT_SBOM.equals(action)) { + exportSbom(request, response); } else if (PortalConstants.CREATE_CLEARING_REQUEST.equals(action)) { createClearingRequest(request, response); } else if (PortalConstants.VIEW_CLEARING_REQUEST.equals(action)) { @@ -252,6 +255,8 @@ public void serveResource(ResourceRequest request, ResourceResponse response) th addLicenseToLinkedReleases(request, response); } else if (PortalConstants.LOAD_SPDX_LICENSE_INFO.equals(action)) { loadSpdxLicenseInfo(request, response); + } else if (PortalConstants.LOAD_SBOM_IMPORT_INFO.equals(action)) { + loadSbomImportInfoFromAttachment(request, response); } else if (isGenericAction(action)) { dealWithGenericAction(request, response, action); } else if (PortalConstants.LOAD_CHANGE_LOGS.equals(action) || PortalConstants.VIEW_CHANGE_LOGS.equals(action)) { @@ -430,13 +435,24 @@ private void removeOrphanObligation(ResourceRequest request, ResourceResponse re } private void importBom(ResourceRequest request, ResourceResponse response) { - ProjectService.Iface projectClient = thriftClients.makeProjectClient(); User user = UserCacheHolder.getUserFromRequest(request); - String attachmentContentId = request.getParameter(ATTACHMENT_CONTENT_ID); + ResourceParameters parameters = request.getResourceParameters(); + String attachmentContentId = parameters.getValue(ATTACHMENT_CONTENT_ID); + String bomtype = parameters.getValue(BOM_TYPE); + String projectId = parameters.getValue(PROJECT_ID); + final RequestSummary requestSummary; try { - final RequestSummary requestSummary = projectClient.importBomFromAttachmentContent(user, attachmentContentId); + boolean isSPDX = "SPDX".equals(bomtype); + ProjectService.Iface projectClient = thriftClients.makeProjectClient(); + if (isSPDX) { + requestSummary = projectClient.importBomFromAttachmentContent(user, attachmentContentId); + } else { + Locale locale = request.getLocale(); + requestSummary = projectClient.importCycloneDxFromAttachmentContent(user, attachmentContentId, projectId); + } + boolean isSuccess = RequestStatus.SUCCESS.equals(requestSummary.getRequestStatus()); String portletId = (String) request.getAttribute(WebKeys.PORTLET_ID); ThemeDisplay tD = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY); long plid = tD.getPlid(); @@ -444,12 +460,29 @@ private void importBom(ResourceRequest request, ResourceResponse response) { LiferayPortletURL projectUrl = PortletURLFactoryUtil.create(request, portletId, plid, PortletRequest.RENDER_PHASE); projectUrl.setParameter(PortalConstants.PAGENAME, PortalConstants.PAGENAME_DETAIL); - projectUrl.setParameter(PortalConstants.PROJECT_ID, requestSummary.getMessage()); - JSONObject jsonObject = JSONFactoryUtil.createJSONObject(); - jsonObject.put("redirectUrl", projectUrl.toString()); + if (isSPDX) { + projectUrl.setParameter(PortalConstants.PROJECT_ID, requestSummary.getMessage()); + jsonObject.put("redirectUrl", projectUrl.toString()); + } else { + try { + if (CommonUtils.isNotNullEmptyOrWhitespace(requestSummary.getMessage())) { + jsonObject = JSONFactoryUtil.createJSONObject(requestSummary.getMessage()); + requestSummary.unsetMessage(); + } + if (isSuccess || RequestStatus.DUPLICATE.equals(requestSummary.getRequestStatus())) { + projectUrl.setParameter(PortalConstants.PROJECT_ID, jsonObject.getString("projectId")); + jsonObject.put("redirectUrl", projectUrl.toString()); + } + } catch (JSONException e) { + log.error("JSON Exception occured, maybe the 'RequestSummary' message is not in JSON form: " + e.getMessage()); + } + } + if (!isSuccess) { + attachmentPortletUtils.deleteAttachments(Sets.newHashSet(attachmentContentId)); + } renderRequestSummary(request, response, requestSummary, jsonObject); } catch (TException e) { log.error("Failed to import BOM.", e); @@ -457,6 +490,44 @@ private void importBom(ResourceRequest request, ResourceResponse response) { } } + private void exportSbom(ResourceRequest request, ResourceResponse response) { + final User user = UserCacheHolder.getUserFromRequest(request); + final ResourceParameters parameters = request.getResourceParameters(); + final String bomtype = parameters.getValue(SBOM_FROMAT); + final String projectId = parameters.getValue(PROJECT_ID); + final Boolean includeSubProjReleases = Boolean.valueOf(CommonUtils.nullToEmptyString(parameters.getValue(PROJECT_WITH_SUBPROJECT))); + final String fileName = EXPORT_SBOM; + String contentType = ContentTypes.APPLICATION_JSON; + String bomString = ""; + + try { + if (CommonUtils.isNotNullEmptyOrWhitespace(projectId)) { + ProjectService.Iface projectClient = thriftClients.makeProjectClient(); + RequestSummary summary = projectClient.exportCycloneDxSbom(projectId, bomtype, includeSubProjReleases, user); + RequestStatus staus = summary.getRequestStatus(); + if (RequestStatus.FAILED_SANITY_CHECK.equals(staus)) { + bomString = "{\"status\": \"" + staus.name() + "\"}"; + } else if (RequestStatus.ACCESS_DENIED.equals(staus)) { + bomString = "{\"status\": \"" + staus.name() + "\", \"message\": \"" + SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE + "\"}"; + } else if (RequestStatus.FAILURE.equals(staus)) { + bomString = "{\"status\": \"" + staus.name() + "\", \"message\": \"" + summary.getMessage() + "\"}"; + } else { + bomString = summary.getMessage(); + if (SW360Constants.XML_FILE_EXTENSION.equalsIgnoreCase(bomtype)) { + contentType = ContentTypes.TEXT_XML; + } + } + } + PortletResponseUtil.sendFile(request, response, fileName, bomString.getBytes(), contentType); + } catch (TException e) { + log.error("An error occured while generating SBOM file for export.", e); + response.setProperty(ResourceResponse.HTTP_STATUS_CODE, Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); + } catch (IOException e) { + log.error("Failed to export SBOM file.", e); + response.setProperty(ResourceResponse.HTTP_STATUS_CODE, Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); + } + } + private void createClearingRequest(ResourceRequest request, ResourceResponse response) throws PortletException { User user = UserCacheHolder.getUserFromRequest(request); ClearingRequest clearingRequest = null; @@ -1484,6 +1555,7 @@ private void prepareStandardView(RenderRequest request) throws IOException { request.setAttribute(CUSTOM_FIELD_PREFERRED_CLEARING_DATE_LIMIT, dateLimit); Integer criticalCount = modClient.getOpenCriticalCrCountByGroup(user.getDepartment()); request.setAttribute(CRITICAL_CR_COUNT, criticalCount); + request.setAttribute(PortalConstants.IS_SBOM_IMPORT_EXPORT_ACCESS_USER, SW360Utils.isUserAtleastDesiredRoleInPrimaryOrSecondaryGroup(user, SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE)); } catch(TException e) { log.error("Error in getting the projectList from backend ", e); } @@ -1623,6 +1695,7 @@ private void prepareDetailView(RenderRequest request, RenderResponse response) t total_vuls.addAll(vuls); } } + request.setAttribute(PortalConstants.IS_SBOM_IMPORT_EXPORT_ACCESS_USER, SW360Utils.isUserAtleastDesiredRoleInPrimaryOrSecondaryGroup(user, SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE)); request.setAttribute(PortalConstants.TOTAL_VULNERABILITY_COUNT, total_vuls.size()); putDirectlyLinkedReleasesInRequest(request, project); Set usingProjects = client.searchLinkingProjects(id, user); @@ -2363,6 +2436,7 @@ private void prepareProjectDuplicate(RenderRequest request) { request.setAttribute(USING_PROJECTS, Collections.emptySet()); request.setAttribute(ALL_USING_PROJECTS_COUNT, 0); request.setAttribute(SOURCE_PROJECT_ID, id); + request.setAttribute(PortalConstants.IS_SBOM_IMPORT_EXPORT_ACCESS_USER, SW360Utils.isUserAtleastDesiredRoleInPrimaryOrSecondaryGroup(user, SW360Constants.SBOM_IMPORT_EXPORT_ACCESS_USER_ROLE)); } else { Project project = new Project(); project.setBusinessUnit(user.getDepartment()); @@ -2898,6 +2972,22 @@ private void addLicenseToLinkedReleases(ResourceRequest request, ResourceRespons } } + private void loadSbomImportInfoFromAttachment(ResourceRequest request, ResourceResponse response) throws IOException, PortletException { + String attachmentContentId = request.getResourceParameters().getValue(ATTACHMENT_CONTENT_ID); + final ProjectService.Iface client = thriftClients.makeProjectClient(); + try { + String importInfo = client.getSbomImportInfoFromAttachmentAsString(attachmentContentId); + JSONObject jsonObject = JSONFactoryUtil.createJSONObject(importInfo); + writeJSON(request, response, jsonObject); + } catch (SW360Exception e) { + log.error("Error fetching sbom import info from attachment db: " + attachmentContentId, e); + } catch (TException e) { + log.error("Error fetching sbom import info from attachment db: " + attachmentContentId, e); + } catch (JSONException e) { + log.error("An exception occured while parsing sbom import info: " + attachmentContentId, e); + } + } + private void loadSpdxLicenseInfo(ResourceRequest request, ResourceResponse response) throws IOException, PortletException { final User user = UserCacheHolder.getUserFromRequest(request); final String releaseId = request.getParameter(PortalConstants.RELEASE_ID); diff --git a/frontend/sw360-portlet/src/main/resources/META-INF/resources/css/components/_tooltips.scss b/frontend/sw360-portlet/src/main/resources/META-INF/resources/css/components/_tooltips.scss index 1a5bf6fa4d..0bb3160098 100644 --- a/frontend/sw360-portlet/src/main/resources/META-INF/resources/css/components/_tooltips.scss +++ b/frontend/sw360-portlet/src/main/resources/META-INF/resources/css/components/_tooltips.scss @@ -117,6 +117,43 @@ display: none; } +/** CycloneDxComponentType **/ +.sw360-tt-CycloneDxComponentType:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-APPLICATION:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-CONTAINER:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-DEVICE:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-FILE:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-FIRMWARE:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-FRAMEWORK:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-LIBRARY:hover:after { + content: attr(data-content); +} + +.sw360-tt-CycloneDxComponentType-OPERATING_SYSTEM:hover:after { + content: attr(data-content); +} + /** Ternary **/ .sw360-tt-Ternary:hover:after { content: attr(data-content); diff --git a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/admin/oauthclient/view.jsp b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/admin/oauthclient/view.jsp index 5ca89952cd..0417d74caf 100644 --- a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/admin/oauthclient/view.jsp +++ b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/admin/oauthclient/view.jsp @@ -377,7 +377,7 @@ $('#copyToClipboard').on('click', function(event) { let textSelector = "#confirmDialog table tr td:eq(1)", textToCopy = $("#confirmDialog table tr td pre#token").text(); - clipboard.copyToClipboard(textToCopy, textSelector, true); + clipboard.copyToClipboard(textToCopy, '#copyToClipboard'); }); clientsTbl.destroy(); loadListOfClient(); diff --git a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/detailOverview.jspf b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/detailOverview.jspf index abf85f21cd..109e0d2b29 100644 --- a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/detailOverview.jspf +++ b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/detailOverview.jspf @@ -212,7 +212,7 @@ $('#copyToClipboard').on('click', function(event) { let textSelector = "table tr td#documentId", textToCopy = $(textSelector).clone().children().remove().end().text().trim(); - clipboard.copyToClipboard(textToCopy, textSelector); + clipboard.copyToClipboard(textToCopy, '#copyToClipboard'); }); let keyComponent, valueComponent; diff --git a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/editBasicInfo.jspf b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/editBasicInfo.jspf index 82b5f5f59b..e380f14bb1 100644 --- a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/editBasicInfo.jspf +++ b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/components/editBasicInfo.jspf @@ -8,6 +8,7 @@ ~ SPDX-License-Identifier: EPL-2.0 --%> +<%@ page import="org.eclipse.sw360.datahandler.thrift.CycloneDxComponentType" %> <%@ page import="org.eclipse.sw360.datahandler.thrift.components.ComponentType" %> <%@ page import="org.eclipse.sw360.datahandler.thrift.components.Component" %> <%@ page import="org.eclipse.sw360.datahandler.thrift.Visibility" %> @@ -64,9 +65,24 @@ + +
+ + + + + + +
+ + +
@@ -74,8 +90,6 @@ value="" placeholder=""/>
- -
@@ -90,13 +104,6 @@ placeholder="" value=""/>
- -
- - " value=""/> -
- @@ -130,6 +137,13 @@ + +
+ + " value=""/> +
+
@@ -137,14 +151,6 @@ placeholder="">
- -
- - " - value="" readonly class="form-control"/> -
-
: + + : + + : diff --git a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/releases/detailOverview.jspf b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/releases/detailOverview.jspf index b8e73083c9..4e1268cb72 100644 --- a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/releases/detailOverview.jspf +++ b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/includes/releases/detailOverview.jspf @@ -251,7 +251,7 @@ $('#copyToClipboard').on('click', function(event) { let textSelector = "table tr td#documentId", textToCopy = $(textSelector).clone().children().remove().end().text().trim(); - clipboard.copyToClipboard(textToCopy, textSelector); + clipboard.copyToClipboard(textToCopy, '#copyToClipboard'); }); diff --git a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/view.jsp b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/view.jsp index 3201acf65b..5118fedf26 100644 --- a/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/view.jsp +++ b/frontend/sw360-portlet/src/main/resources/META-INF/resources/html/components/view.jsp @@ -184,7 +184,7 @@