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 Oct 4, 2024
1 parent 9bf7c2b commit abc1735
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,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 @@ -186,15 +186,14 @@ 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 @@ -224,6 +223,9 @@ 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();
List<String> packagesToBeRemoved = new ArrayList<>();
List<String> packagesToBeKept = new ArrayList<>();

for (org.cyclonedx.model.Component comp : components) {
if (CommonUtils.isNullOrEmptyCollection(comp.getExternalReferences())
Expand All @@ -243,8 +245,20 @@ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent a

try {
AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user);
packagesToBeKept.add(pkgAddSummary.getId());
componentsWithoutVcs.add(fullName);

if(replacePackageFlag && CommonUtils.isNotEmpty(projectPkgIds)){
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);
}
}
}

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

if(replacePackageFlag){
for(String pkgId: packagesToBeKept){
packagesToBeRemoved.remove(pkgId);
}
unlinkPackageAndReleaseFromProject(project, packagesToBeRemoved);
}

RequestStatus updateStatus = projectDatabaseHandler.updateProject(project, user);
if (RequestStatus.SUCCESS.equals(updateStatus)) {
log.info("linking packages to project successfull: " + projId);
Expand Down Expand Up @@ -359,7 +380,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 @@ -412,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 @@ -542,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) throws SW360Exception {

final var countMap = new HashMap<String, Integer>();
final Set<String> duplicateComponents = new HashSet<>();
Expand All @@ -551,15 +572,17 @@ 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();
String relName = "";
AddDocumentRequestSummary compAddSummary;
List<String> packagesToBeRemoved = new ArrayList<>();
List<String> packagesToBeKept = new ArrayList<>();
try {
compAddSummary = componentDatabaseHandler.addComponent(comp, user.getEmail());

Expand Down Expand Up @@ -647,6 +670,18 @@ private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.c

try {
AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user);
packagesToBeKept.add(pkgAddSummary.getId());
if(replacePackageFlag && !CommonUtils.isNullOrEmptyMap(releaseRelationMap)){
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);
}
}
}

if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) {
pkg.setId(pkgAddSummary.getId());
if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) {
Expand Down Expand Up @@ -680,6 +715,12 @@ private Map<String, String> importAllComponentsAsPackages(Map<String, List<org.c
log.error("An error occured while creating/adding component from SBOM: " + e.getMessage());
continue;
}
if(replacePackageFlag){
for(String pkgId: packagesToBeKept){
packagesToBeRemoved.remove(pkgId);
}
unlinkPackageAndReleaseFromProject(project, packagesToBeRemoved);
}
}

project.setReleaseIdToUsage(releaseRelationMap);
Expand Down Expand Up @@ -1011,4 +1052,32 @@ public static boolean containsComp(Map<String, List<org.cyclonedx.model.Componen
}
return false;
}

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){
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);
}
}
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 @@ -863,6 +863,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 @@ -1932,7 +1932,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 @@ -1986,22 +1986,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 = false) 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 @@ -933,9 +933,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 @@ -550,7 +550,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 @@ -2381,6 +2381,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", TestHelper.generateAuthHeader(testUserId, testUserPassword));
this.mockMvc.perform(builder).andExpect(status().isOk()).andDo(this.documentationHandler.document());
}
Expand Down

0 comments on commit abc1735

Please sign in to comment.