From 9f45c16313b7b840ee5ae36ee7a510e41945c75d Mon Sep 17 00:00:00 2001 From: "sameed.ahmad" Date: Wed, 15 May 2024 17:40:40 +0530 Subject: [PATCH] feat(importCDX): Add functionality to configure release creation when importing SBOM to an existing project Signed-off-by: sameed.ahmad --- .../sw360/cyclonedx/CycloneDxBOMImporter.java | 74 ++++++++++++++++--- .../db/ProjectDatabaseHandler.java | 8 +- .../sw360/projects/ProjectHandler.java | 7 ++ .../src/main/thrift/projects.thrift | 6 ++ .../src/docs/asciidoc/projects.adoc | 8 ++ .../project/ProjectController.java | 8 +- .../project/Sw360ProjectService.java | 4 +- .../restdocs/ProjectSpecTest.java | 3 +- 8 files changed, 100 insertions(+), 18 deletions(-) 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 index 98b739d4b4..2f9084ce0f 100644 --- 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 @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.ArrayList; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -155,7 +156,7 @@ private Map> getVcsToComponentMap(Li } @SuppressWarnings("unchecked") - public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent attachmentContent, String projectId, User user) { + public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent attachmentContent, String projectId, User user, boolean replacePackageFlag) { RequestSummary requestSummary = new RequestSummary(); Map messageMap = new HashMap<>(); requestSummary.setRequestStatus(RequestStatus.FAILURE); @@ -194,16 +195,15 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a if (!IS_PACKAGE_PORTLET_ENABLED) { vcsToComponentMap.put("", components); - requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent); + requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent, replacePackageFlag); } else { vcsToComponentMap = getVcsToComponentMap(components); if (componentsCount == vcsCount) { - requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent); + requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent, replacePackageFlag); } else if (componentsCount > vcsCount) { - - requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent); + requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent, replacePackageFlag); if (requestSummary.requestStatus.equals(RequestStatus.SUCCESS)) { @@ -233,6 +233,7 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a packages = ""; } Project project = projectDatabaseHandler.getProjectById(projId, user); + Set projectPkgIds = CommonUtils.isNullOrEmptyCollection(project.getPackageIds()) ? new HashSet<>() : project.getPackageIds(); for (org.cyclonedx.model.Component comp : components) { if (CommonUtils.isNullOrEmptyCollection(comp.getExternalReferences()) @@ -253,6 +254,19 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user); componentsWithoutVcs.add(fullName); + if(replacePackageFlag && CommonUtils.isNotEmpty(projectPkgIds)){ + List packagesToBeRemoved = new ArrayList<>(); + for(String pkgId: projectPkgIds){ + Package existingPkg = packageDatabaseHandler.getPackageById(pkgId); + String existingPkgPURLWithoutVersion = existingPkg.getPurl().split("@")[0]; + String currPkgPURLWithoutVersion = pkg.getPurl().split("@")[0]; + if(currPkgPURLWithoutVersion.equals(existingPkgPURLWithoutVersion) && !pkg.getVersion().equals(existingPkg.getVersion())){ + packagesToBeRemoved.add(pkgId); + } + } + unlinkPackageAndReleaseFromProject(project, packagesToBeRemoved); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) { pkg.setId(pkgAddSummary.getId()); if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) { @@ -366,7 +380,7 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a } public RequestSummary importSbomAsProject(org.cyclonedx.model.Component compMetadata, - Map> vcsToComponentMap, String projectId, AttachmentContent attachmentContent) + Map> vcsToComponentMap, String projectId, AttachmentContent attachmentContent, boolean replacePackageFlag) throws SW360Exception { final RequestSummary summary = new RequestSummary(); summary.setRequestStatus(RequestStatus.FAILURE); @@ -420,7 +434,7 @@ public RequestSummary importSbomAsProject(org.cyclonedx.model.Component compMeta } if (IS_PACKAGE_PORTLET_ENABLED) { - messageMap = importAllComponentsAsPackages(vcsToComponentMap, project); + messageMap = importAllComponentsAsPackages(vcsToComponentMap, project, replacePackageFlag); } else { messageMap = importAllComponentsAsReleases(vcsToComponentMap, project); } @@ -550,7 +564,7 @@ private Map importAllComponentsAsReleases(Map importAllComponentsAsPackages(Map> vcsToComponentMap, Project project) { + private Map importAllComponentsAsPackages(Map> vcsToComponentMap, Project project, boolean replacePackageFlag) { final var countMap = new HashMap(); final Set duplicateComponents = new HashSet<>(); @@ -559,10 +573,10 @@ private Map importAllComponentsAsPackages(Map invalidReleases = new HashSet<>(); final Set invalidPackages = new HashSet<>(); final Map releaseRelationMap = CommonUtils.isNullOrEmptyMap(project.getReleaseIdToUsage()) ? new HashMap<>() : project.getReleaseIdToUsage(); + Set projectPkgIds = CommonUtils.isNullOrEmptyCollection(project.getPackageIds()) ? new HashSet<>() : project.getPackageIds(); countMap.put(REL_CREATION_COUNT_KEY, 0); countMap.put(REL_REUSE_COUNT_KEY, 0); countMap.put(PKG_CREATION_COUNT_KEY, 0); countMap.put(PKG_REUSE_COUNT_KEY, 0); int relCreationCount = 0, relReuseCount = 0, pkgCreationCount = 0, pkgReuseCount = 0; - for (Map.Entry> entry : vcsToComponentMap.entrySet()) { Component comp = createComponent(entry.getKey()); Release release = new Release(); @@ -595,6 +609,7 @@ private Map importAllComponentsAsPackages(Map importAllComponentsAsPackages(Map packagesToBeRemoved = new ArrayList<>(); + for(String pkgId: projectPkgIds){ + Package existingPkg = packageDatabaseHandler.getPackageById(pkgId); + String existingPkgPURLWithoutVersion = existingPkg.getPurl().split("@")[0]; + String currPkgPURLWithoutVersion = pkg.getPurl().split("@")[0]; + if(currPkgPURLWithoutVersion.equals(existingPkgPURLWithoutVersion) && !pkg.getVersion().equals(existingPkg.getVersion())){ + packagesToBeRemoved.add(pkgId); + } + } + unlinkPackageAndReleaseFromProject(project, packagesToBeRemoved); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) { pkg.setId(pkgAddSummary.getId()); if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) { @@ -976,4 +1004,32 @@ public String getComponetNameById(String id, User user) throws SW360Exception { Component comp = componentDatabaseHandler.getComponent(id, user); return comp.getName(); } + + public void unlinkPackageAndReleaseFromProject(Project project, List packagesToBeRemoved) throws SW360Exception { + Map releaseRelationMap = CommonUtils.isNullOrEmptyMap(project.getReleaseIdToUsage()) ? new HashMap<>() : project.getReleaseIdToUsage(); + Set projectPkgIds = CommonUtils.isNullOrEmptyCollection(project.getPackageIds()) ? new HashSet<>() : project.getPackageIds(); + + for (String pkgIdToBeRemoved: packagesToBeRemoved){ + String linkedReleaseId = packageDatabaseHandler.getPackageById(pkgIdToBeRemoved).getReleaseId(); + + projectPkgIds.remove(pkgIdToBeRemoved); + boolean unlinkRelease = true; + if(CommonUtils.isNotNullEmptyOrWhitespace(linkedReleaseId)){ + for(String pkgId: projectPkgIds){ + Package pkg = packageDatabaseHandler.getPackageById(pkgId); + if(CommonUtils.isNotNullEmptyOrWhitespace(pkg.getReleaseId()) && pkg.getReleaseId().equals(linkedReleaseId)){ + unlinkRelease = false; + break; + } + }; + } + + if(CommonUtils.isNotNullEmptyOrWhitespace(linkedReleaseId) && unlinkRelease){ + releaseRelationMap.remove(linkedReleaseId); + } + } + + project.setPackageIds(projectPkgIds); + project.setReleaseIdToUsage(releaseRelationMap); + } } 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 028e3efb4f..50a311cefe 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 @@ -828,7 +828,7 @@ private boolean isLinkedReleasesUpdateFromLinkedPackagesFailed(Project updatedPr */ for (Map.Entry> entry : releaseIdToPackageIdsMap.entrySet()) { if (targetMap.containsKey(entry.getKey()) && - CommonUtils.isNotEmpty(CommonUtils.nullToEmptySet(Sets.intersection(entry.getValue(), unlinkedPacakgeIds)))) { + !CommonUtils.isNotEmpty(CommonUtils.nullToEmptySet(Sets.intersection(entry.getValue(), unlinkedPacakgeIds)))) { targetMap.remove(entry.getKey()); } } @@ -1845,6 +1845,10 @@ public RequestSummary importBomFromAttachmentContent(User user, String attachmen } public RequestSummary importCycloneDxFromAttachmentContent(User user, String attachmentContentId, String projectId) throws SW360Exception { + return importCycloneDxFromAttachmentContent(user, attachmentContentId, projectId, true); + } + + public RequestSummary importCycloneDxFromAttachmentContent(User user, String attachmentContentId, String projectId, boolean replacePackageFlag) throws SW360Exception { final AttachmentContent attachmentContent = attachmentConnector.getAttachmentContent(attachmentContentId); final Duration timeout = Duration.durationOf(30, TimeUnit.SECONDS); try { @@ -1853,7 +1857,7 @@ public RequestSummary importCycloneDxFromAttachmentContent(User user, String att .unsafeGetAttachmentStream(attachmentContent)) { final CycloneDxBOMImporter cycloneDxBOMImporter = new CycloneDxBOMImporter(this, componentDatabaseHandler, packageDatabaseHandler, attachmentConnector, user); - return cycloneDxBOMImporter.importFromBOM(inputStream, attachmentContent, projectId, user); + return cycloneDxBOMImporter.importFromBOM(inputStream, attachmentContent, projectId, user, replacePackageFlag); } } catch (IOException e) { log.error("Error while importing / parsing CycloneDX SBOM! ", e); 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 fdfdb391ba..7464e1b52d 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 @@ -275,6 +275,13 @@ public RequestSummary importCycloneDxFromAttachmentContent(User user, String att return handler.importCycloneDxFromAttachmentContent(user, attachmentContentId, projectId); } + @Override + public RequestSummary importCycloneDxFromAttachmentContentWithReplacePackageFlag(User user, String attachmentContentId, String projectId, boolean replacePackageFlag) throws SW360Exception { + assertId(attachmentContentId); + assertUser(user); + return handler.importCycloneDxFromAttachmentContent(user, attachmentContentId, projectId, replacePackageFlag); + } + @Override public RequestSummary exportCycloneDxSbom(String projectId, String bomType, boolean includeSubProjReleases, User user) throws SW360Exception { assertId(projectId); diff --git a/libraries/datahandler/src/main/thrift/projects.thrift b/libraries/datahandler/src/main/thrift/projects.thrift index f94ef2d980..1a8f0db54a 100644 --- a/libraries/datahandler/src/main/thrift/projects.thrift +++ b/libraries/datahandler/src/main/thrift/projects.thrift @@ -610,6 +610,12 @@ service ProjectService { */ RequestSummary importCycloneDxFromAttachmentContent(1: User user, 2: string attachmentContentId, 3: string projectId) throws (1: SW360Exception exp); + /** + * Parse a CycloneDx SBoM file (XML or JSON) and write the information to SW360 as Project / Component / Release / Package + * with replacePackageFlag + */ + RequestSummary importCycloneDxFromAttachmentContentWithReplacePackageFlag(1: User user, 2: string attachmentContentId, 3: string projectId, 4: bool replacePackageFlag) throws (1: SW360Exception exp); + /** * Export a CycloneDx SBoM file (XML or JSON) for a Project */ diff --git a/rest/resource-server/src/docs/asciidoc/projects.adoc b/rest/resource-server/src/docs/asciidoc/projects.adoc index dd9f23f3d1..a4bd700a44 100644 --- a/rest/resource-server/src/docs/asciidoc/projects.adoc +++ b/rest/resource-server/src/docs/asciidoc/projects.adoc @@ -801,6 +801,14 @@ include::{snippets}/should_document_import_cyclonedx/http-response.adoc[] A `POST` request is used to import a SBOM on a project. Currently only CycloneDX(.xml/.json) files are supported. +[red]#Request parameter# +|=== +|Parameter |Description + +|replacePackageFlag +|When true, it replaces existing packages and release with the latest versions; when false, it adds new packages alongside existing versions without replacing them. +|=== + [red]#Request body# |=== Type |Description diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java index 3b4e739abd..4c38875f91 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java @@ -1778,7 +1778,7 @@ public ResponseEntity importSBOM( } projectId = requestSummary.getMessage(); } else { - requestSummary = projectService.importCycloneDX(sw360User, attachment.getAttachmentContentId(), ""); + requestSummary = projectService.importCycloneDX(sw360User, attachment.getAttachmentContentId(), "", false); if (requestSummary.getRequestStatus() == RequestStatus.FAILURE) { return new ResponseEntity(requestSummary.getMessage(), HttpStatus.BAD_REQUEST); @@ -1832,14 +1832,14 @@ public ResponseEntity importSBOMonProject( @Parameter(description = "Project ID", example = "376576") @PathVariable(value = "id", required = true) String id, @Parameter(description = "SBOM file") - @RequestBody MultipartFile file + @RequestBody MultipartFile file, + @RequestParam(value = "replacePackageFlag", required = false) boolean replacePackageFlag ) throws TException { final User sw360User = restControllerHelper.getSw360UserFromAuthentication(); Attachment attachment = null; final RequestSummary requestSummary; String projectId = null; Map messageMap = new HashMap<>(); - try { attachment = attachmentService.uploadAttachment(file, new Attachment(), sw360User); } catch (IOException e) { @@ -1847,7 +1847,7 @@ public ResponseEntity importSBOMonProject( throw new RuntimeException("failed to upload attachment", e); } - requestSummary = projectService.importCycloneDX(sw360User, attachment.getAttachmentContentId(), id); + requestSummary = projectService.importCycloneDX(sw360User, attachment.getAttachmentContentId(), id, replacePackageFlag); if (requestSummary.getRequestStatus() == RequestStatus.FAILURE) { return new ResponseEntity(requestSummary.getMessage(), HttpStatus.BAD_REQUEST); diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java index 547abc8e89..8c9df0c430 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java @@ -859,9 +859,9 @@ public RequestSummary importSPDX(User user, String attachmentContentId) throws T * @return RequestSummary * @throws TException */ - public RequestSummary importCycloneDX(User user, String attachmentContentId, String projectId) throws TException { + public RequestSummary importCycloneDX(User user, String attachmentContentId, String projectId, boolean replacePackageFlag) throws TException { ProjectService.Iface sw360ProjectClient = getThriftProjectClient(); - return sw360ProjectClient.importCycloneDxFromAttachmentContent(user, attachmentContentId, CommonUtils.nullToEmptyString(projectId)); + return sw360ProjectClient.importCycloneDxFromAttachmentContentWithReplacePackageFlag(user, attachmentContentId, CommonUtils.nullToEmptyString(projectId), replacePackageFlag); } /** diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java index 0994916f9f..9d1e93e76f 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java @@ -553,7 +553,7 @@ public void before() throws TException, IOException { given(this.projectServiceMock.loadPreferredClearingDateLimit()).willReturn(Integer.valueOf(7)); given(this.projectServiceMock.importSPDX(any(),any())).willReturn(requestSummaryForSPDX); - given(this.projectServiceMock.importCycloneDX(any(),any(),any())).willReturn(requestSummaryForCycloneDX); + given(this.projectServiceMock.importCycloneDX(any(),any(),any(),anyBoolean())).willReturn(requestSummaryForCycloneDX); given(this.sw360ReportServiceMock.getDocumentName(any(), any())).willReturn(projectName); given(this.sw360ReportServiceMock.getProjectBuffer(any(),anyBoolean(),any())).willReturn(ByteBuffer.allocate(10000)); given(this.projectServiceMock.getProjectsForUser(any(), any())).willReturn(projectList); @@ -2329,6 +2329,7 @@ public void should_document_import_cyclonedx_on_project() throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/api/projects/"+project.getId()+"/import/SBOM") .content(file.getBytes()) .contentType(MediaType.MULTIPART_FORM_DATA) + .queryParam("replacePackageFlag", "true") .header("Authorization", "Bearer " + accessToken); this.mockMvc.perform(builder).andExpect(status().isOk()).andDo(this.documentationHandler.document()); }