Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(importCDX): Add functionality to configure release creation when importing SBOM to an existing project #2458

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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