Skip to content

Commit

Permalink
feat(importCDX): Add functionality to configure release creation when…
Browse files Browse the repository at this point in the history
… importing SBOM to an existing project

Signed-off-by: sameed.ahmad <[email protected]>
  • Loading branch information
sameed20 committed Jun 5, 2024
1 parent 20c818a commit c9aa23f
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -155,7 +156,7 @@ private Map<String, List<org.cyclonedx.model.Component>> 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<String, String> messageMap = new HashMap<>();
requestSummary.setRequestStatus(RequestStatus.FAILURE);
Expand Down Expand Up @@ -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)) {

Expand Down Expand Up @@ -233,6 +233,7 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a
packages = "";
}
Project project = projectDatabaseHandler.getProjectById(projId, user);
Set<String> projectPkgIds = CommonUtils.isNullOrEmptyCollection(project.getPackageIds()) ? new HashSet<>() : project.getPackageIds();

for (org.cyclonedx.model.Component comp : components) {
if (CommonUtils.isNullOrEmptyCollection(comp.getExternalReferences())
Expand All @@ -253,6 +254,18 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a
AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user);
componentsWithoutVcs.add(fullName);

if(replacePackageFlag && CommonUtils.isNotEmpty(projectPkgIds)){
List<String> packagesToBeRemoved = new ArrayList<>();
for(String pkgId: projectPkgIds){
Package existingPkg = packageDatabaseHandler.getPackageById(pkgId);
if(pkg.getName().equals(existingPkg.getName()) && !pkg.getVersion().equals(existingPkg.getVersion())){
log.error("existing pkg and release need to be replaced " + existingPkg.getId() );
packagesToBeRemoved.add(pkgId);
}
}
unlinkPackageAndReleaseFromProject(project, packagesToBeRemoved);
}

if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) {
pkg.setId(pkgAddSummary.getId());
if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) {
Expand Down Expand Up @@ -366,7 +379,7 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a
}

public RequestSummary importSbomAsProject(org.cyclonedx.model.Component compMetadata,
Map<String, List<org.cyclonedx.model.Component>> vcsToComponentMap, String projectId, AttachmentContent attachmentContent)
Map<String, List<org.cyclonedx.model.Component>> vcsToComponentMap, String projectId, AttachmentContent attachmentContent, boolean replacePackageFlag)
throws SW360Exception {
final RequestSummary summary = new RequestSummary();
summary.setRequestStatus(RequestStatus.FAILURE);
Expand Down Expand Up @@ -420,7 +433,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);
}
Expand Down Expand Up @@ -550,7 +563,7 @@ private Map<String, String> importAllComponentsAsReleases(Map<String, List<org.c
return messageMap;
}

private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.cyclonedx.model.Component>> vcsToComponentMap, Project project) {
private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.cyclonedx.model.Component>> vcsToComponentMap, Project project, boolean replacePackageFlag) {

final var countMap = new HashMap<String, Integer>();
final Set<String> duplicateComponents = new HashSet<>();
Expand All @@ -559,10 +572,10 @@ private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.c
final Set<String> invalidReleases = new HashSet<>();
final Set<String> invalidPackages = new HashSet<>();
final Map<String, ProjectReleaseRelationship> releaseRelationMap = CommonUtils.isNullOrEmptyMap(project.getReleaseIdToUsage()) ? new HashMap<>() : project.getReleaseIdToUsage();
Set<String> 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<String, List<org.cyclonedx.model.Component>> entry : vcsToComponentMap.entrySet()) {
Component comp = createComponent(entry.getKey());
Release release = new Release();
Expand Down Expand Up @@ -595,6 +608,7 @@ private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.c

try {
AddDocumentRequestSummary relAddSummary = componentDatabaseHandler.addRelease(release, user);

if (CommonUtils.isNotNullEmptyOrWhitespace(relAddSummary.getId())) {
release.setId(relAddSummary.getId());
if (AddDocumentRequestStatus.SUCCESS.equals(relAddSummary.getRequestStatus())) {
Expand Down Expand Up @@ -643,6 +657,18 @@ private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.c

try {
AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user);
if(replacePackageFlag && !CommonUtils.isNullOrEmptyMap(releaseRelationMap)){
List<String> packagesToBeRemoved = new ArrayList<>();
for(String pkgId: projectPkgIds){
Package existingPkg = packageDatabaseHandler.getPackageById(pkgId);
if(pkg.getName().equals(existingPkg.getName()) && !pkg.getVersion().equals(existingPkg.getVersion())){
log.error("existing pkg and release need to be replaced " + existingPkg.getId() );
packagesToBeRemoved.add(pkgId);
}
}
unlinkPackageAndReleaseFromProject(project, packagesToBeRemoved);
}

if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) {
pkg.setId(pkgAddSummary.getId());
if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) {
Expand Down Expand Up @@ -976,4 +1002,36 @@ public String getComponetNameById(String id, User user) throws SW360Exception {
Component comp = componentDatabaseHandler.getComponent(id, user);
return comp.getName();
}

public void unlinkPackageAndReleaseFromProject(Project project, List<String> packagesToBeRemoved) throws SW360Exception {
Map<String, ProjectReleaseRelationship> releaseRelationMap = CommonUtils.isNullOrEmptyMap(project.getReleaseIdToUsage()) ? new HashMap<>() : project.getReleaseIdToUsage();
Set<String> projectPkgIds = CommonUtils.isNullOrEmptyCollection(project.getPackageIds()) ? new HashSet<>() : project.getPackageIds();

for (String pkgIdToBeRemoved: packagesToBeRemoved){
//fetching the linked release form a pkg
String linkedReleaseId = packageDatabaseHandler.getPackageById(pkgIdToBeRemoved).getReleaseId();

//Removing the pkg
projectPkgIds.remove(pkgIdToBeRemoved);

//removing the release linked from pkg only if the other packages in project are not linked to this rel
boolean isReleaseUnlinkPossible = true;
if(CommonUtils.isNotNullEmptyOrWhitespace(linkedReleaseId)){
for(String pkgId: projectPkgIds){
Package pkg = packageDatabaseHandler.getPackageById(pkgId);
if(CommonUtils.isNotNullEmptyOrWhitespace(pkg.getReleaseId()) && pkg.getReleaseId().equals(linkedReleaseId)){
isReleaseUnlinkPossible = false;
break;
}
};
}

if(CommonUtils.isNotNullEmptyOrWhitespace(linkedReleaseId) && isReleaseUnlinkPossible){
releaseRelationMap.remove(linkedReleaseId);
}
}

project.setPackageIds(projectPkgIds);
project.setReleaseIdToUsage(releaseRelationMap);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ private boolean isLinkedReleasesUpdateFromLinkedPackagesFailed(Project updatedPr
*/
for (Map.Entry<String, Set<String>> 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());
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions libraries/datahandler/src/main/thrift/projects.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
8 changes: 8 additions & 0 deletions rest/resource-server/src/docs/asciidoc/projects.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>(requestSummary.getMessage(), HttpStatus.BAD_REQUEST);
Expand Down Expand Up @@ -1832,22 +1832,22 @@ 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 = true) boolean replacePackageFlag
) throws TException {
final User sw360User = restControllerHelper.getSw360UserFromAuthentication();
Attachment attachment = null;
final RequestSummary requestSummary;
String projectId = null;
Map<String, String> messageMap = new HashMap<>();

try {
attachment = attachmentService.uploadAttachment(file, new Attachment(), sw360User);
} catch (IOException e) {
log.error("failed to upload attachment", e);
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<String>(requestSummary.getMessage(), HttpStatus.BAD_REQUEST);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());
}
Expand Down

0 comments on commit c9aa23f

Please sign in to comment.