Skip to content
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 @@ -2940,6 +2940,21 @@ public RequestSummary importBomFromAttachmentContent(User user, String attachmen
}
}

public SpdxImportDryRunResult dryRunImportBom(User user, 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)) {
final SpdxBOMImporterSink spdxBOMImporterSink = new SpdxBOMImporterSink(user, null, this);
final SpdxBOMImporter spdxBOMImporter = new SpdxBOMImporter(spdxBOMImporterSink);
return spdxBOMImporter.dryRunImportSpdxBOMAsRelease(inputStream, attachmentContent);
}
} catch (IOException e) {
throw new SW360Exception(e.getMessage());
}
}

private String getFileType(String fileName) {
if (isNullEmptyOrWhitespace(fileName) || !fileName.contains(".")) {
log.error("Can not get file type from file name - no file extension");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,144 @@ public ImportBomRequestPreparation prepareImportSpdxBOMAsRelease(File targetFile
return requestPreparation;
}

public SpdxImportDryRunResult dryRunImportSpdxBOMAsRelease(InputStream inputStream, AttachmentContent attachmentContent)
throws SW360Exception, IOException {
final SpdxImportDryRunResult result = new SpdxImportDryRunResult();
List<SpdxComponentInfo> newComponents = new ArrayList<>();
List<SpdxComponentInfo> existingComponents = new ArrayList<>();
List<LicenseConflictInfo> licenseConflicts = new ArrayList<>();
List<String> warnings = new ArrayList<>();

String fileType = getFileType(attachmentContent.getFilename());
if (!"rdf".equals(fileType) && !"spdx".equals(fileType)) {
result.setRequestStatus(RequestStatus.FAILURE);
result.setMessage("Invalid file type. Only .rdf and .spdx files are supported.");
return result;
}
final String ext = "." + fileType;

final File sourceFile = DatabaseHandlerUtil.saveAsTempFile(inputStream, attachmentContent.getId(), ext);
try {
SpdxDocument spdxDocument = openAsSpdx(sourceFile);
if (spdxDocument == null) {
result.setRequestStatus(RequestStatus.FAILURE);
result.setMessage("Failed to parse SPDX file.");
return result;
}

List<SpdxElement> describedPackages = spdxDocument.getDocumentDescribes().stream().collect(Collectors.toList());
List<SpdxElement> packages = describedPackages.stream()
.filter(SpdxPackage.class::isInstance)
.collect(Collectors.toList());

if (packages.isEmpty()) {
result.setRequestStatus(RequestStatus.FAILURE);
result.setMessage("The provided BOM did not contain any top level packages.");
return result;
} else if (packages.size() > 1) {
result.setRequestStatus(RequestStatus.FAILURE);
result.setMessage("The provided BOM file contained multiple described top level packages. This is not allowed here.");
return result;
}

final SpdxPackage spdxPackage = (SpdxPackage) packages.get(0);
final String componentName = getValue(spdxPackage.getName());
final String version = getValue(spdxPackage.getVersionInfo());
final String spdxId = spdxPackage.getId();

Set<String> licenseConcluded = new HashSet<>();
if (spdxPackage.getLicenseConcluded() != null) {
licenseConcluded.add(spdxPackage.getLicenseConcluded().toString());
}

Set<String> licenseDeclared = new HashSet<>();
String declaredLicense = createLicenseDeclaredFromSpdxLicenseDeclared(spdxPackage);
if (declaredLicense != null) {
licenseDeclared.add(declaredLicense);
}

SpdxComponentInfo componentInfo = new SpdxComponentInfo();
componentInfo.setName(componentName);
componentInfo.setVersion(version);
componentInfo.setComponentType(ComponentType.OSS.toString());
componentInfo.setSpdxId(spdxId);
componentInfo.setLicenseConcluded(licenseConcluded);
componentInfo.setLicenseDeclared(licenseDeclared);

Component existingComponent = sink.searchComponent(componentName);
if (existingComponent != null) {
existingComponents.add(componentInfo);

Set<String> existingLicenses = existingComponent.getMainLicenseIds();
if (existingLicenses != null && !existingLicenses.isEmpty()) {
for (String existingLicense : existingLicenses) {
for (String proposedLicense : licenseConcluded) {
if (!existingLicense.equals(proposedLicense) && !"NOASSERTION".equals(proposedLicense)) {
LicenseConflictInfo conflict = new LicenseConflictInfo();
conflict.setComponentName(componentName);
conflict.setExistingLicense(existingLicense);
conflict.setProposedLicense(proposedLicense);
conflict.setConflictType("LICENSE_MISMATCH");
licenseConflicts.add(conflict);
}
}
}
}
} else {
newComponents.add(componentInfo);
}

List<SpdxPackage> allPackages = getPackages(spdxDocument);
for (SpdxPackage pkg : allPackages) {
String pkgName = getValue(pkg.getName());
if (pkgName == null || pkgName.equals(componentName)) {
continue;
}

SpdxComponentInfo pkgInfo = new SpdxComponentInfo();
pkgInfo.setName(pkgName);
pkgInfo.setVersion(getValue(pkg.getVersionInfo()));
pkgInfo.setComponentType(ComponentType.OSS.toString());
pkgInfo.setSpdxId(pkg.getId());

Set<String> pkgLicenseConcluded = new HashSet<>();
if (pkg.getLicenseConcluded() != null) {
pkgLicenseConcluded.add(pkg.getLicenseConcluded().toString());
}
pkgInfo.setLicenseConcluded(pkgLicenseConcluded);

String pkgDeclared = createLicenseDeclaredFromSpdxLicenseDeclared(pkg);
if (pkgDeclared != null) {
Set<String> pkgLicenseDeclared = new HashSet<>();
pkgLicenseDeclared.add(pkgDeclared);
pkgInfo.setLicenseDeclared(pkgLicenseDeclared);
}

Component existingPkgComponent = sink.searchComponent(pkgName);
if (existingPkgComponent != null) {
existingComponents.add(pkgInfo);
} else {
newComponents.add(pkgInfo);
}
}

result.setNewComponents(newComponents);
result.setExistingComponents(existingComponents);
result.setLicenseConflicts(licenseConflicts);
result.setWarnings(warnings);
result.setRequestStatus(RequestStatus.SUCCESS);
result.setMessage("Dry-run completed successfully. " + newComponents.size() + " new components, " +
existingComponents.size() + " existing components, " + licenseConflicts.size() + " license conflicts.");

} catch (InvalidSPDXAnalysisException | NullPointerException e) {
log.error("Error during dry-run import: " + e);
result.setRequestStatus(RequestStatus.FAILURE);
result.setMessage("Error analyzing SPDX file: " + e.getMessage());
}

return result;
}

public RequestSummary importSpdxBOMAsRelease(InputStream inputStream, AttachmentContent attachmentContent, User user)
throws SW360Exception, IOException {
return importSpdxBOM(inputStream, attachmentContent, SW360Constants.TYPE_RELEASE, user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,13 @@ public ImportBomRequestPreparation prepareImportBom(User user, String attachment
return handler.prepareImportBom(user, attachmentContentId);
}

@Override
public SpdxImportDryRunResult dryRunImportBom(User user, String attachmentContentId) throws TException {
assertNotNull(attachmentContentId);
assertUser(user);
return handler.dryRunImportBom(user, attachmentContentId);
}

@Override
public RequestSummary importBomFromAttachmentContent(User user, String attachmentContentId) throws TException {
assertNotNull(attachmentContentId);
Expand Down
9 changes: 9 additions & 0 deletions libraries/datahandler/src/main/thrift/components.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ typedef sw360.SW360Exception SW360Exception
typedef sw360.PaginationData PaginationData
typedef sw360.ClearingReportStatus ClearingReportStatus
typedef sw360.ImportBomRequestPreparation ImportBomRequestPreparation
typedef sw360.SpdxImportDryRunResult SpdxImportDryRunResult
typedef sw360.SpdxComponentInfo SpdxComponentInfo
typedef sw360.LicenseConflictInfo LicenseConflictInfo
typedef attachments.Attachment Attachment
typedef attachments.FilledAttachment FilledAttachment
typedef users.User User
Expand Down Expand Up @@ -1038,6 +1041,12 @@ service ComponentService {

ImportBomRequestPreparation prepareImportBom(1: User user, 2:string attachmentContentId);

/**
* Dry-run SPDX import - simulates import without persisting data
* Returns detailed impact analysis including new/existing components and license conflicts
**/
SpdxImportDryRunResult dryRunImportBom(1: User user, 2:string attachmentContentId);

/**
* parse a bom file and write the information to SW360
**/
Expand Down
25 changes: 25 additions & 0 deletions libraries/datahandler/src/main/thrift/sw360.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ struct ImportBomRequestPreparation {
7: optional string message;
}

struct SpdxImportDryRunResult {
1: required RequestStatus requestStatus;
2: optional list<SpdxComponentInfo> newComponents;
3: optional list<SpdxComponentInfo> existingComponents;
4: optional list<LicenseConflictInfo> licenseConflicts;
5: optional list<string> warnings;
6: optional string message;
}

struct SpdxComponentInfo {
1: optional string name;
2: optional string version;
3: optional string componentType;
4: optional string spdxId;
5: optional set<string> licenseConcluded;
6: optional set<string> licenseDeclared;
}

struct LicenseConflictInfo {
1: optional string componentName;
2: optional string existingLicense;
3: optional string proposedLicense;
4: optional string conflictType;
}

struct CustomProperties {
1: optional string id,
2: optional string revision,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.eclipse.sw360.datahandler.thrift.RequestStatus;
import org.eclipse.sw360.datahandler.thrift.RequestSummary;
import org.eclipse.sw360.datahandler.thrift.ImportBomRequestPreparation;
import org.eclipse.sw360.datahandler.thrift.SpdxImportDryRunResult;
import org.eclipse.sw360.datahandler.thrift.RestrictedResource;
import org.eclipse.sw360.datahandler.thrift.Source;
import org.eclipse.sw360.datahandler.thrift.VerificationStateInfo;
Expand Down Expand Up @@ -1068,6 +1069,58 @@ public ResponseEntity<?> importSBOM(
return new ResponseEntity<>(halResource, status);
}

@Operation(
summary = "Dry-run SPDX import to analyze impact without persisting data.",
description = "Performs a dry-run import of an SPDX/SBOM file to analyze the impact without writing to the database.",
responses = {
@ApiResponse(
responseCode = "200", description = "Dry-run analysis completed.",
content = {
@Content(mediaType = "application/json",
schema = @Schema(implementation = SpdxImportDryRunResult.class))
}
),
@ApiResponse(
responseCode = "400", description = "Invalid SBOM file."
),
@ApiResponse(
responseCode = "500", description = "Internal server error."
)
},
tags = {"Components"}
)
@RequestMapping(value = COMPONENTS_URL + "/import/SBOM/dryRun", method = RequestMethod.POST, consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<?> dryRunImportSBOM(
@Parameter(description = "Type of SBOM being uploaded.",
schema = @Schema(type = "string", allowableValues = {"SPDX"})
)
@RequestParam(value = "type", required = true) String type,
@Parameter(description = "The file to be uploaded.")
@RequestBody MultipartFile file
) throws TException {
final User sw360User = restControllerHelper.getSw360UserFromAuthentication();
Attachment attachment;
final SpdxImportDryRunResult dryRunResult;
if(!type.equalsIgnoreCase("SPDX") || !attachmentService.isValidSbomFile(file, type)) {
throw new IllegalArgumentException("SBOM file is not valid. It currently only supports SPDX(.rdf/.spdx) files.");
}
try {
attachment = attachmentService.uploadAttachment(file, new Attachment(), sw360User);
try {
dryRunResult = componentService.dryRunImportSBOM(sw360User, attachment.getAttachmentContentId());
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
} catch (IOException e) {
log.error("failed to upload attachment", e);
throw new RuntimeException("failed to upload attachment", e);
}
if (!(dryRunResult.getRequestStatus() == RequestStatus.SUCCESS)) {
throw new BadRequestClientException("Invalid SBOM file: " + dryRunResult.getMessage());
}
return new ResponseEntity<>(dryRunResult, HttpStatus.OK);
}

@Operation(
summary = "Import SBOM in SPDX format.",
description = "Import SBOM in SPDX format.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ public RequestSummary importSBOM(User user, String attachmentContentId) throws T
return sw360ComponentClient.importBomFromAttachmentContent(user, attachmentContentId);
}

public SpdxImportDryRunResult dryRunImportSBOM(User user, String attachmentContentId) throws TException {
ComponentService.Iface sw360ComponentClient = getThriftComponentClient();
return sw360ComponentClient.dryRunImportBom(user, attachmentContentId);
}

public ImportBomRequestPreparation prepareImportSBOM(User user, String attachmentContentId) throws TException {
ComponentService.Iface sw360ComponentClient = getThriftComponentClient();
return sw360ComponentClient.prepareImportBom(user, attachmentContentId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public Sw360Module() {
setMixInAnnotation(ModerationRequest.class, Sw360Module.ModerationRequestMixin.class);
setMixInAnnotation(EmbeddedModerationRequest.class, Sw360Module.EmbeddedModerationRequestMixin.class);
setMixInAnnotation(ImportBomRequestPreparation.class, Sw360Module.ImportBomRequestPreparationMixin.class);
setMixInAnnotation(SpdxImportDryRunResult.class, Sw360Module.SpdxImportDryRunResultMixin.class);
setMixInAnnotation(ModerationPatch.class, Sw360Module.ModerationPatchMixin.class);
setMixInAnnotation(ProjectDTO.class, Sw360Module.ProjectDTOMixin.class);
setMixInAnnotation(EmbeddedProjectDTO.class, Sw360Module.EmbeddedProjectDTOMixin.class);
Expand Down Expand Up @@ -2740,6 +2741,18 @@ static abstract class EmbeddedModerationRequestMixin extends ModerationRequestMi
public static abstract class ImportBomRequestPreparationMixin extends ImportBomRequestPreparation {
}

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties({
"setRequestStatus",
"setNewComponents",
"setExistingComponents",
"setLicenseConflicts",
"setWarnings",
"setMessage"
})
public static abstract class SpdxImportDryRunResultMixin extends SpdxImportDryRunResult {
}

@JsonInclude(JsonInclude.Include.NON_NULL)
public static abstract class ModerationPatchMixin extends ModerationPatch {
}
Expand Down