componentDetails = result.getFileDetails();
+ if (componentDetails == null) {
+ log.warn("Null scan file details found. Skipping: {}", result);
+ return;
+ }
+ for (ScanFileDetails componentDetail : componentDetails ) {
+ if (componentDetail == null) {
+ log.warn("Null scan file component details found. Skipping: {}", result);
+ continue;
+ }
+ ScanFileDetails newFileDetails = createUpdatedResultDetails(componentDetail, newPurl);
+ result.getFileDetails().set(0, newFileDetails);
+ log.debug("Updated package URL from {} to {} for file: {}",
+ componentDetail.getPurls()[0],
+ newPurl,
+ result.getFilePath());
+ }
+ }
+
+ /**
+ * Creates a PackageURL object from a replacement rule's target URL.
+ *
+ * @param rule The replacement rule containing the new package URL
+ * @return A new PackageURL object, or null
if the URL is malformed
+ */
+ private PackageURL createPackageUrl(@NonNull ReplaceRule rule) {
+ try {
+ return new PackageURL(rule.getReplaceWith());
+ } catch (MalformedPackageURLException e) {
+ log.warn("Failed to parse PURL from replace rule: {}. Skipping", rule);
+ }
+ return null;
+ }
+
+
+ /**
+ * Updates the component details with new package information while preserving existing metadata.
+ * Takes the existing component as the base and only overrides specific fields (component name,
+ * vendor, PURLs) based on the new package URL. License details will be updated if specified
+ * in the replacement rule.
+ *
+ * @param existingComponent The current component details to use as a base
+ * @param newPurl The new package URL containing updated package information
+ * @return Updated component details with specific fields overridden
+ */
+ private ScanFileDetails createUpdatedResultDetails(ScanFileDetails existingComponent,
+ PackageURL newPurl) {
+ // Check for cached component
+ ScanFileDetails cached = purl2ComponentDetailsMap.get(newPurl.toString());
+
+ if (cached != null) {
+ return cached.toBuilder()
+ .file(existingComponent.getFile())
+ .fileHash(existingComponent.getFileHash())
+ .fileUrl(existingComponent.getFileUrl())
+ .purls(new String[]{newPurl.toString()})
+ .component(newPurl.getName())
+ .vendor(newPurl.getNamespace())
+ .build();
+ }
+ // If no cached info, create minimal version
+ return existingComponent.toBuilder()
+ .copyrightDetails(new CopyrightDetails[]{})
+ .licenseDetails(new LicenseDetails[]{})
+ .vulnerabilityDetails(new VulnerabilityDetails[]{})
+ .version(null)
+ .purls(new String[]{newPurl.toString()})
+ .url(Purl2Url.isSupported(newPurl) ? Purl2Url.convert(newPurl) : "")
+ .component(newPurl.getName())
+ .vendor(newPurl.getNamespace())
+ .build();
+ }
+
+
+ /**
+ * Applies remove rules to scan results, filtering out matches based on certain criteria.
+ *
+ * First, matches are found based on path and/or purl:
+ * - Rule must match either both path and purl, just the path, or just the purl
+ *
+ * Then, for each matched result:
+ * 1. If none of the matching rules define line ranges -> Remove the result
+ * 2. If any matching rules define line ranges -> Only remove if the result's lines overlap with any rule's line range
+ *
+ * @param results The list of scan results to process
+ * @param rules The list of remove rules to apply
+ */
+ public void applyRemoveRules(@NonNull List results, @NonNull List rules) {
+ log.debug("Starting remove rules application to {} results", results.size());
+ results.removeIf(result -> matchesRemovalCriteria(result, rules));
+ log.debug("Remove rules application completed. Results remaining: {}", results.size());
+ }
+
+ /**
+ * Determines if a scan result should be excluded based on the removal rules.
+ *
+ * For each rule, checks:
+ * 1. If the result matches the rule's path/purl patterns
+ * 2. Then applies line range logic:
+ * - If rule has no line range specified: Result is removed
+ * - If rule has line range specified: Result is only removed if its lines overlap with the rule's range
+ *
+ * @param result The scan result to evaluate
+ * @param rules List of removal rules to check against
+ * @return true if the result should be removed, false otherwise
+ */
+ private Boolean matchesRemovalCriteria(@NonNull ScanFileResult result, @NonNull List rules) {
+ // Make sure it's a valid result before processing
+ if (hasInvalidStructure(result)) {
+ log.warn("Scan result has invalid structure - missing required fields for file: {}", result.getFilePath());
+ return false;
+ }
+
+ if (hasNoValidMatch(result)) {
+ log.debug("Scan result has no valid matches for file: {}", result.getFilePath());
+ return false;
+ }
+
+ return rules.stream()
+ .filter(rule -> isMatchingRule(result, rule))
+ .anyMatch(rule -> {
+ // Process line range conditions:
+ // - returns true if rule has no line range specified
+ // - returns true if rule has line range AND result overlaps with it
+ // - returns false otherwise (continue checking remaining rules)
+ boolean ruleHasLineRange = rule.getStartLine() != null && rule.getEndLine() != null;
+ return !ruleHasLineRange || isRemoveLineRangeMatch(rule, result);
+ });
+ }
+
+ /**
+ * Checks if a scan result matches the path and/or PURL patterns defined in a rule.
+ * The match is considered successful if any of these conditions are met:
+ * 1. Rule has both a path and PURL defined: Both must match the result
+ * 2. Rule has only a PURL defined: PURL must match the result
+ * 3. Rule has only a path defined: Path must match the result
+ *
+ * @param result The scan result to check
+ * @param rule The rule containing the patterns to match against
+ * @param Type parameter extending Rule class
+ * @return true if the result matches the rule's patterns according to above conditions
+ */
+ private Boolean isMatchingRule(@NonNull ScanFileResult result, @NonNull T rule) {
+ // Check if rule has valid path and/or PURL patterns
+ boolean hasPath = rule.getPath() != null && !rule.getPath().isEmpty();
+ boolean hasPurl = rule.getPurl() != null && !rule.getPurl().isEmpty();
+
+ // Check three possible matching conditions:
+
+ // 1. Both path and PURL match
+ if (hasPath && hasPurl) {
+ return isPathAndPurlMatch(rule, result);
+ }
+ // 2. Only PURL match required and matches
+ if (hasPurl) {
+ return isPurlOnlyMatch(rule, result);
+ }
+ // 3. Only path match required and matches
+ if (hasPath) {
+ return isPathOnlyMatch(rule, result);
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if the line range of the remove rule match the result
+ *
+ * @param rule Remove Rule
+ * @param result Scan file Result
+ * @return true
if remove rule is in range, false
otherwise
+ */
+ private boolean isRemoveLineRangeMatch(@NonNull RemoveRule rule, @NonNull ScanFileResult result) {
+ LineRange ruleLineRange = new LineRange(rule.getStartLine(), rule.getEndLine());
+
+ String lines = result.getFileDetails().get(0).getLines();
+ List resultLineRanges = LineRangeUtils.parseLineRanges(lines);
+
+ return LineRangeUtils.hasOverlappingRanges(resultLineRanges,ruleLineRange);
+ }
+
+ /**
+ * Checks if both path and purl of the rule match the result
+ *
+ * @param rule BOM Rule
+ * @param result Scan file Result
+ * @return true
if it matches, false
otherwise
+ */
+ private boolean isPathAndPurlMatch(@NonNull Rule rule, @NonNull ScanFileResult result) {
+ return result.getFilePath().startsWith(rule.getPath()) &&
+ isPurlMatch(rule.getPurl(), result.getFileDetails().get(0).getPurls());
+ }
+
+
+ /**
+ * Checks if the rule's path matches the result (ignoring purl)
+ *
+ * @param rule BOM Rule
+ * @param result Scan file Results
+ * @return true
if it matches, false
otherwise
+ */
+ private boolean isPathOnlyMatch(@NonNull Rule rule, @NonNull ScanFileResult result) {
+ return result.getFilePath().startsWith(rule.getPath());
+ }
+
+ /**
+ * Checks if the rule's purl matches the result (ignoring path)
+ *
+ * @param rule BOM Rule
+ * @param result Scan file Result
+ * @return true
if it matches, false
otherwise
+ */
+ private boolean isPurlOnlyMatch(@NonNull Rule rule, @NonNull ScanFileResult result) {
+ return isPurlMatch(rule.getPurl(), result.getFileDetails().get(0).getPurls());
+ }
+
+ /**
+ * Checks if a specific purl exists in an array of purls
+ *
+ * @param rulePurl PURL from Rule
+ * @param resultPurls List of Scan file Result PURLs
+ * @return true
if it matches, false
otherwise
+ */
+ private boolean isPurlMatch(String rulePurl, String[] resultPurls) {
+ if (rulePurl == null || resultPurls == null) {
+ return false;
+ }
+ for (String resultPurl : resultPurls) {
+ if (Objects.equals(rulePurl, resultPurl)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if a scan result contains the minimum required data structure for processing.
+ * This validation ensures that:
+ * 1. The result has a valid file path identifier
+ * 2. The result contains a non-empty list of scan details
+ * 3. The primary scan detail entry (first in the list) exists
+ *
+ * This structural validation is a prerequisite for any further processing of scan results,
+ * such as match analysis or rule processing. Without these basic elements, the scan result
+ * cannot be meaningfully processed.
+ *
+ * @param result The scan result to validate
+ * @return true if the basic structure is invalid, false if valid
+ */
+ private boolean hasInvalidStructure(@NonNull ScanFileResult result) {
+ String filepath = result.getFilePath();
+ List details = result.getFileDetails();
+ return filepath == null ||
+ details == null ||
+ details.isEmpty() ||
+ details.get(0) == null;
+ }
+
+ /**
+ * Checks if the scan result has a valid match.
+ * A result is considered to have no valid match if:
+ * - Match type is 'none'
+ * - Lines array is null
+ * - PURLs array is null or empty
+ *
+ * @param result The scan result to validate
+ * @return true if there's no valid match, false if there is a valid match
+ */
+ private boolean hasNoValidMatch(@NonNull ScanFileResult result) {
+ if (hasInvalidStructure(result)) {
+ return true;
+ }
+
+ ScanFileDetails firstDetail = result.getFileDetails().get(0);
+ return firstDetail.getMatchType() == MatchType.none ||
+ firstDetail.getLines() == null ||
+ firstDetail.getPurls() == null ||
+ firstDetail.getPurls().length == 0;
+ }
+
+}
diff --git a/src/main/java/com/scanoss/Winnowing.java b/src/main/java/com/scanoss/Winnowing.java
index 19e8742..303d8eb 100644
--- a/src/main/java/com/scanoss/Winnowing.java
+++ b/src/main/java/com/scanoss/Winnowing.java
@@ -209,33 +209,6 @@ private Boolean skipSnippets(@NonNull String filename, char[] contents) {
}
}
}
- // TODO do we still want this?
- // Check to see if the first newline is very far away. If so, it's another hint this could be a binary/data file
-// for (int i = 0; i < contents.length; i++) {
-// if (contents[i] == '\n') {
-// return false;
-// } else if (i > MAX_LONG_LINE_CHARS) {
-// log.trace("Skipping snippets due to file line being too long: {} - {}", filename, MAX_LONG_LINE_CHARS);
-// return true;
-// }
-// }
- // TODO do we want to skip a whole file is some of it is a large single line?
-// StringBuilder outputBuilder = new StringBuilder();
-// for (char c: contents) {
-// if (c == '\n') { // New line, check line length
-// if (outputBuilder.length() > MAX_LONG_LINE_CHARS) {
-// log.trace("Skipping snippets due to file line being too long: {} - {}", filename, MAX_LONG_LINE_CHARS);
-// return true;
-// }
-// outputBuilder.setLength(0); // empty the string again
-// } else {
-// outputBuilder.append(c);
-// }
-// }
-// if (outputBuilder.length() > MAX_LONG_LINE_CHARS) { // Check the last string length
-// log.trace("Skipping snippets due to file line being too long: {} - {}", filename, MAX_LONG_LINE_CHARS);
-// return true;
-// }
return false;
}
diff --git a/src/main/java/com/scanoss/cli/ScanCommandLine.java b/src/main/java/com/scanoss/cli/ScanCommandLine.java
index 86ff835..3b8ddd1 100644
--- a/src/main/java/com/scanoss/cli/ScanCommandLine.java
+++ b/src/main/java/com/scanoss/cli/ScanCommandLine.java
@@ -23,8 +23,11 @@
package com.scanoss.cli;
import com.scanoss.Scanner;
+import com.scanoss.ScannerPostProcessor;
+import com.scanoss.dto.ScanFileResult;
import com.scanoss.exceptions.ScannerException;
import com.scanoss.exceptions.WinnowingException;
+import com.scanoss.settings.Settings;
import com.scanoss.utils.JsonUtils;
import com.scanoss.utils.ProxyUtils;
import lombok.NonNull;
@@ -33,12 +36,14 @@
import java.io.IOException;
import java.net.Proxy;
import java.nio.file.Files;
+import java.nio.file.Paths;
import java.time.Duration;
import java.util.List;
import static com.scanoss.ScanossConstants.*;
import static com.scanoss.cli.CommandLine.printDebug;
import static com.scanoss.cli.CommandLine.printMsg;
+import static com.scanoss.utils.JsonUtils.toScanFileResultJsonObject;
/**
* Scan Command Line Processor Class
@@ -91,6 +96,9 @@ class ScanCommandLine implements Runnable {
@picocli.CommandLine.Option(names = {"-n", "--ignore"}, description = "Ignore components specified in the SBOM file")
private String ignoreSbom;
+ @picocli.CommandLine.Option(names = {"--settings"}, description = "Settings file to use for scanning (optional - default scanoss.json)")
+ private String settingsPath;
+
@picocli.CommandLine.Option(names = {"--snippet-limit"}, description = "Length of single line snippet limit (0 for unlimited, default 1000)")
private int snippetLimit = 1000;
@@ -108,6 +116,7 @@ class ScanCommandLine implements Runnable {
private Scanner scanner;
+ private Settings settings;
/**
* Run the 'scan' command
*/
@@ -140,6 +149,12 @@ public void run() {
throw new RuntimeException("Error: Failed to setup proxy config");
}
}
+
+ if(settingsPath != null && !settingsPath.isEmpty()) {
+ settings = Settings.createFromPath(Paths.get(settingsPath));
+ if (settings == null) throw new RuntimeException("Error: Failed to read settings file");
+ }
+
if (com.scanoss.cli.CommandLine.debug) {
if (numThreads != DEFAULT_WORKER_THREADS) {
printMsg(err, String.format("Running with %d threads.", numThreads));
@@ -164,7 +179,9 @@ public void run() {
.hiddenFilesFolders(allHidden).numThreads(numThreads).url(apiUrl).apiKey(apiKey)
.retryLimit(retryLimit).timeout(Duration.ofSeconds(timeoutLimit)).scanFlags(scanFlags)
.sbomType(sbomType).sbom(sbom).snippetLimit(snippetLimit).customCert(caCertPem).proxy(proxy).hpsm(enableHpsm)
+ .settings(this.settings)
.build();
+
File f = new File(fileFolder);
if (!f.exists()) {
throw new RuntimeException(String.format("Error: File or folder does not exist: %s\n", fileFolder));
@@ -180,6 +197,7 @@ public void run() {
/**
* Load the specified file into a string
+ *
* @param filename filename to load
* @return loaded string
*/
@@ -220,7 +238,6 @@ private void scanFile(String file) {
}
throw new RuntimeException(String.format("Something went wrong while scanning %s", file));
}
-
/**
* Scan the specified folder/directory and return the results
*
diff --git a/src/main/java/com/scanoss/dto/LicenseDetails.java b/src/main/java/com/scanoss/dto/LicenseDetails.java
index a230a6c..14cbc05 100644
--- a/src/main/java/com/scanoss/dto/LicenseDetails.java
+++ b/src/main/java/com/scanoss/dto/LicenseDetails.java
@@ -23,7 +23,10 @@
package com.scanoss.dto;
import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
import static com.scanoss.utils.JsonUtils.checkBooleanString;
@@ -31,17 +34,20 @@
* Scan Results Match License Details
*/
@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
public class LicenseDetails {
- private final String name;
- private final String source;
- private final String copyleft;
+ private String name;
+ private String source;
+ private String copyleft;
@SerializedName("patent_hints")
- private final String patentHints;
- private final String url;
+ private String patentHints;
+ private String url;
@SerializedName("checklist_url")
- private final String checklistUrl;
+ private String checklistUrl;
@SerializedName("osadl_updated")
- private final String osadlUpdated;
+ private String osadlUpdated;
/**
* Determine if the license is Copyleft or not
diff --git a/src/main/java/com/scanoss/dto/ScanFileDetails.java b/src/main/java/com/scanoss/dto/ScanFileDetails.java
index b343d40..9cbfb53 100644
--- a/src/main/java/com/scanoss/dto/ScanFileDetails.java
+++ b/src/main/java/com/scanoss/dto/ScanFileDetails.java
@@ -23,14 +23,21 @@
package com.scanoss.dto;
import com.google.gson.annotations.SerializedName;
+import com.scanoss.dto.enums.MatchType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
/**
* Scan File Result Detailed Information
*/
@Data
+@Builder(toBuilder = true)
+@AllArgsConstructor
public class ScanFileDetails {
- private final String id;
+ @SerializedName("id")
+ private final MatchType matchType;
private final String component;
private final String vendor;
private final String version;
diff --git a/src/main/java/com/scanoss/dto/enums/MatchType.java b/src/main/java/com/scanoss/dto/enums/MatchType.java
new file mode 100644
index 0000000..0d8bb33
--- /dev/null
+++ b/src/main/java/com/scanoss/dto/enums/MatchType.java
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.dto.enums;
+
+/**
+ * Represents the type of match found during SCANOSS scanning.
+ */
+public enum MatchType {
+ /**
+ * Indicates a complete file match
+ */
+ file,
+
+ /**
+ * Indicates a partial code snippet match
+ */
+ snippet,
+
+ /**
+ * Indicates no match was found
+ */
+ none
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/rest/ScanApi.java b/src/main/java/com/scanoss/rest/ScanApi.java
index a843763..92c41ef 100644
--- a/src/main/java/com/scanoss/rest/ScanApi.java
+++ b/src/main/java/com/scanoss/rest/ScanApi.java
@@ -238,8 +238,19 @@ private RequestBody multipartData(Map data, String uuid) {
}
private static final int RETRY_FAIL_SLEEP_TIME = 5; // Time to sleep between failed scan requests
+
+ /**
+ * Base URL for the SCANOSS OSSKB (Open Source Knowledge Base) free API.
+ * This endpoint provides access to the free tier of SCANOSS scanning services.
+ */
public static final String DEFAULT_BASE_URL = "https://api.osskb.org";
+
+ /**
+ * Base URL for the SCANOSS Premium API.
+ * This endpoint is used for premium/enterprise level scanning services.
+ */
public static final String DEFAULT_BASE_URL2 = "https://api.scanoss.com";
+
static final String DEFAULT_SCAN_PATH = "scan/direct";
static final String DEFAULT_SCAN_URL = String.format( "%s/%s", DEFAULT_BASE_URL, DEFAULT_SCAN_PATH ); // Free OSS OSSKB URL
static final String DEFAULT_SCAN_URL2 = String.format( "%s/%s", DEFAULT_BASE_URL2, DEFAULT_SCAN_PATH ); // Standard SCANOSS Premium URL
diff --git a/src/main/java/com/scanoss/settings/Bom.java b/src/main/java/com/scanoss/settings/Bom.java
new file mode 100644
index 0000000..2b13f6c
--- /dev/null
+++ b/src/main/java/com/scanoss/settings/Bom.java
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.settings;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.Singular;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Represents the Bill of Materials (BOM) rules configuration for the SCANOSS scanner.
+ * This class defines rules for modifying the scan results through include,
+ * ignore, remove, and replace operations.
+ */
+@Data
+@Builder
+public class Bom {
+ /**
+ * List of include rules for adding context when scanning.
+ * These rules are sent to the SCANOSS API and have a higher chance of being
+ * considered part of the resulting scan.
+ */
+ private final @Singular("include") List include;
+ /**
+ * List of ignore rules for excluding certain components .
+ * These rules are sent to the SCANOSS API.
+ */
+ private final @Singular("ignore") List ignore;
+ /**
+ * List of remove rules for excluding components from results after scanning.
+ * These rules are applied to the results file after scanning and are processed
+ * on the client side.
+ */
+ private final @Singular("remove") List remove;
+ /**
+ * List of replace rules for substituting components after scanning.
+ * These rules are applied to the results file after scanning and are processed
+ * on the client side. Each rule can specify a new PURL and license for the
+ * replacement component.
+ */
+ private final @Singular("replace") List replace;
+
+ /**
+ * Cached list of replace rules sorted by priority.
+ * This list is lazily initialized when first accessed through
+ * getReplaceRulesByPriority().
+ *
+ * @see #getReplaceRulesByPriority()
+ */
+ private final List sortedReplace;
+
+ /**
+ * Sorts replace rules by priority from highest to lowest:
+ * 1. Rules with both purl/path (most specific)
+ * 3. Rules with only purl
+ * 4. Rules with only path (least specific)
+ *
+ * @return A new list containing the sorted replacement rules
+ */
+ public List getReplaceRulesByPriority() {
+ if (sortedReplace == null) {
+ List sortedReplace = new ArrayList<>(replace);
+ sortedReplace.sort(new RuleComparator());
+ return sortedReplace;
+ }
+ return sortedReplace;
+ }
+
+ /**
+ * Get the size of the Remove rules
+ *
+ * @return size of remove list or zero
+ */
+ public int getRemoveSize() {
+ return remove != null ? remove.size() : 0;
+ }
+
+ /**
+ * Get the size of the Replace rules
+ *
+ * @return size of replace rules or zero
+ */
+ public int getReplaceSize() {
+ return replace != null ? replace.size() : 0;
+ }
+}
+
diff --git a/src/main/java/com/scanoss/settings/RemoveRule.java b/src/main/java/com/scanoss/settings/RemoveRule.java
new file mode 100644
index 0000000..3e35188
--- /dev/null
+++ b/src/main/java/com/scanoss/settings/RemoveRule.java
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.settings;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * Rule for removing specific components from scan results.
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@SuperBuilder
+public class RemoveRule extends Rule {
+ @SerializedName("start_line")
+ private final Integer startLine;
+ @SerializedName("end_line")
+ private final Integer endLine;
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/settings/ReplaceRule.java b/src/main/java/com/scanoss/settings/ReplaceRule.java
new file mode 100644
index 0000000..1e88265
--- /dev/null
+++ b/src/main/java/com/scanoss/settings/ReplaceRule.java
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.settings;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * Rule for replacing components in scan results.
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@SuperBuilder
+public class ReplaceRule extends Rule {
+ @SerializedName("replace_with")
+ private final String replaceWith;
+ private final String license;
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/settings/Rule.java b/src/main/java/com/scanoss/settings/Rule.java
new file mode 100644
index 0000000..0b18942
--- /dev/null
+++ b/src/main/java/com/scanoss/settings/Rule.java
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.settings;
+
+import lombok.Data;
+import lombok.experimental.SuperBuilder;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Base class for SCANOSS BOM rules. Rules are used to modify scan results
+ * based on file paths and Package URLs (PURLs).
+ *
+ * Rules support two types of matching:
+ * - Full match: Both path and PURL match
+ * - Partial match: Either path or PURL matches
+ *
+ */
+@Data
+@Slf4j
+@SuperBuilder()
+public class Rule {
+ private final String path;
+ private final String purl;
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/settings/RuleComparator.java b/src/main/java/com/scanoss/settings/RuleComparator.java
new file mode 100644
index 0000000..a45cb9b
--- /dev/null
+++ b/src/main/java/com/scanoss/settings/RuleComparator.java
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.settings;
+
+import lombok.NonNull;
+
+import java.util.Comparator;
+
+
+/**
+ * A comparator implementation for sorting {@link Rule} objects based on their attributes
+ * and a defined priority system.
+ *
+ * The comparison is performed using the following criteria in order:
+ *
+ * - Priority score based on the presence of PURL and path attributes
+ * - Path length comparison (if priority scores are equal)
+ *
+ *
+ * Priority scoring system:
+ *
+ * - Score 4: Rule has both PURL and path
+ * - Score 2: Rule has only PURL
+ * - Score 1: Rule has only path
+ * - Score 0: Rule has neither PURL nor path
+ *
+ */
+public class RuleComparator implements Comparator {
+
+ /**
+ * Compares two Rule objects based on their priority scores and path lengths.
+ *
+ * The comparison follows these steps:
+ *
+ * - Calculate priority scores for both rules
+ * - If scores differ, higher score takes precedence
+ * - If scores are equal and both rules have paths, longer path takes precedence
+ * - If no differentiation is possible, rules are considered equal
+ *
+ *
+ * @param r1 the first Rule to compare
+ * @param r2 the second Rule to compare
+ * @return a negative integer, zero, or a positive integer if the first rule is less than,
+ * equal to, or greater than the second rule respectively
+ */
+ @Override
+ public int compare(@NonNull Rule r1, @NonNull Rule r2) {
+ int score1 = calculatePriorityScore(r1);
+ int score2 = calculatePriorityScore(r2);
+
+ // If scores are different, use the score comparison
+ if (score1 != score2) {
+ return Integer.compare(score2, score1);
+ }
+
+ String r1Path = r1.getPath();
+ String r2Path = r2.getPath();
+
+ // If both rules have paths and scores are equal, compare path lengths
+ if (r1Path != null && r2Path != null) {
+ return Integer.compare(r2Path.length(), r1Path.length());
+ }
+
+ return 0;
+ }
+
+
+ /**
+ * Calculates a priority score for a rule based on its attributes.
+ *
+ * The scoring system is as follows:
+ *
+ * - 4 points: Rule has both PURL and path
+ * - 2 points: Rule has only PURL
+ * - 1 point: Rule has only path
+ * - 0 points: Rule has neither PURL nor path
+ *
+ *
+ * @param rule the Rule object to calculate the score for
+ * @return an integer representing the priority score of the rule
+ */
+ private int calculatePriorityScore(Rule rule) {
+ int score = 0;
+
+ boolean hasPurl = rule.getPurl() != null;
+ boolean hasPath = rule.getPath() != null;
+
+ // Highest priority: has all fields
+ if (hasPurl && hasPath) {
+ score = 4;
+ }
+ // Second priority: has purl only
+ else if (hasPurl) {
+ score = 2;
+ }
+ // Lowest priority: has path only
+ else if (hasPath) {
+ score = 1;
+ }
+
+ return score;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/settings/Settings.java b/src/main/java/com/scanoss/settings/Settings.java
new file mode 100644
index 0000000..c8ca386
--- /dev/null
+++ b/src/main/java/com/scanoss/settings/Settings.java
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.settings;
+
+import com.google.gson.Gson;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Represents the SCANOSS scanner settings configuration.
+ * Provides functionality to load and manage scanner settings from JSON files or strings.
+ * Settings include BOM (Bill of Materials) rules for modifying components before and after scanning.
+ */
+@Slf4j
+@Data
+@Builder
+public class Settings {
+ /**
+ * The Bill of Materials (BOM) configuration containing rules for component handling.
+ * Includes rules for including, ignoring, removing, and replacing components
+ * during and after the scanning process.
+ */
+ private final Bom bom;
+
+ /**
+ * Creates a Settings object from a JSON string
+ *
+ * @param json The JSON string to parse
+ * @return A new Settings object
+ */
+ public static Settings createFromJsonString(@NonNull String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, Settings.class);
+ }
+
+ /**
+ * Creates a Settings object from a JSON file
+ *
+ * @param path The path to the JSON file
+ * @return A new Settings object
+ */
+ public static Settings createFromPath(@NonNull Path path) {
+ try {
+ String json = Files.readString(path, StandardCharsets.UTF_8);
+ return createFromJsonString(json);
+ } catch (IOException e) {
+ log.error("Cannot read settings file - {}", e.getMessage());
+ return null;
+ }
+
+ }
+}
+
diff --git a/src/main/java/com/scanoss/utils/JsonUtils.java b/src/main/java/com/scanoss/utils/JsonUtils.java
index e0ba703..11eb21e 100644
--- a/src/main/java/com/scanoss/utils/JsonUtils.java
+++ b/src/main/java/com/scanoss/utils/JsonUtils.java
@@ -179,6 +179,49 @@ public static List toScanFileResults(@NonNull List resul
return scanFileResults;
}
+
+ /**
+ * Convert a list of ScanFileResult objects to a list of raw JSON strings
+ *
+ * @param results List of ScanFileResult objects to convert
+ * @return List of raw JSON strings
+ * @throws JsonParseException JSON Parsing failed
+ * @throws IllegalStateException JSON field is not of JSON Object type
+ */
+ public static List toRawJsonString(@NonNull List results) throws JsonParseException, IllegalStateException {
+ List rawJsonStrings = new ArrayList<>(results.size());
+ Gson gson = new Gson();
+
+ results.forEach(result -> {
+ JsonObject jsonObject = new JsonObject();
+ JsonElement detailsJson = gson.toJsonTree(result.getFileDetails());
+ jsonObject.add(result.getFilePath(), detailsJson);
+ rawJsonStrings.add(jsonObject.toString());
+ });
+
+ return rawJsonStrings;
+ }
+
+ /**
+ * Converts a list of ScanFileResult objects into a JSON object where the file paths are keys
+ * and the corresponding file details are the values
+ *
+ * @param scanFileResults List of ScanFileResult objects to convert
+ * @return JsonObject containing file paths as keys and file details as JSON elements
+ */
+ public static JsonObject toScanFileResultJsonObject(List scanFileResults) {
+ JsonObject root = new JsonObject();
+ Gson gson = new Gson();
+
+ scanFileResults.forEach(result -> {
+ JsonElement detailsJson = gson.toJsonTree(result.getFileDetails());
+ root.add(result.getFilePath(), detailsJson);
+ });
+
+ return root;
+ }
+
+
/**
* Convert the given JSON Object to a list of Scan File Results
*
@@ -195,6 +238,8 @@ public static List toScanFileResultsFromObject(@NonNull JsonObje
return results;
}
+
+
/**
* Determine if the given string is a boolean true/false
*
diff --git a/src/main/java/com/scanoss/utils/LineRange.java b/src/main/java/com/scanoss/utils/LineRange.java
new file mode 100644
index 0000000..47f647e
--- /dev/null
+++ b/src/main/java/com/scanoss/utils/LineRange.java
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.utils;
+
+import lombok.Getter;
+
+/**
+ * Represents a line range with start and end lines
+ */
+@Getter
+public class LineRange {
+ private final int start;
+ private final int end;
+
+
+ /**
+ * Creates a new line range with the specified start and end lines.
+ *
+ * @param start the starting line number (inclusive)
+ * @param end the ending line number (inclusive)
+ */
+ public LineRange(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Determines if this line range overlaps with another line range.
+ * Two ranges overlap if any line numbers are shared between them.
+ * For example:
+ * - LineRange(1,5) overlaps with LineRange(3,7)
+ * - LineRange(1,3) overlaps with LineRange(3,5)
+ * - LineRange(1,3) does not overlap with LineRange(4,6)
+ *
+ * @param other the LineRange to check for overlap with this range
+ * @return true if the ranges share any line numbers, false otherwise
+ */
+ public boolean overlaps(LineRange other) {
+ return this.start <= other.end && this.end >= other.start;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/utils/LineRangeUtils.java b/src/main/java/com/scanoss/utils/LineRangeUtils.java
new file mode 100644
index 0000000..da2d40e
--- /dev/null
+++ b/src/main/java/com/scanoss/utils/LineRangeUtils.java
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.utils;
+
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class for handling line range operations
+ */
+@Slf4j
+public class LineRangeUtils {
+ /**
+ * Parses a line range string into a list of intervals
+ *
+ * @param lineRanges String in format "1-5,7-10"
+ * @return List of LineInterval objects
+ */
+ public static List parseLineRanges(String lineRanges) {
+ if (lineRanges == null || lineRanges.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+ String[] ranges = lineRanges.split(",");
+ List intervals = new ArrayList<>(ranges.length);
+ for (String range : ranges) {
+ String[] bounds = range.trim().split("-");
+ if (bounds.length == 2) {
+ try {
+ int start = Integer.parseInt(bounds[0].trim());
+ int end = Integer.parseInt(bounds[1].trim());
+ intervals.add(new LineRange(start, end));
+ } catch (NumberFormatException e) {
+ // Skip invalid intervals
+ log.debug("Skipping invalid interval format: {} in range ({}): {}", range, bounds, e.getMessage());
+ }
+ }
+ }
+ return intervals;
+ }
+
+ /**
+ * Checks if a list of line ranges overlaps with a single range
+ *
+ * @param ranges List of line ranges to check against
+ * @param range Single line range to check for overlap
+ * @return true if any interval from the list overlaps with the given range
+ * @throws NullPointerException if either parameter is null
+ */
+ public static boolean hasOverlappingRanges(@NonNull List ranges, @NonNull LineRange range) {
+ for (LineRange interval1 : ranges) {
+ if (interval1.overlaps(range)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/scanoss/utils/Purl2Url.java b/src/main/java/com/scanoss/utils/Purl2Url.java
new file mode 100644
index 0000000..aa52209
--- /dev/null
+++ b/src/main/java/com/scanoss/utils/Purl2Url.java
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss.utils;
+
+import com.github.packageurl.PackageURL;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Converts Package URLs (purls) to their corresponding browsable web URLs for
+ * different package management systems and source code repositories.
+ */
+@Slf4j
+public class Purl2Url {
+
+ /**
+ * PURL type to URL enum
+ */
+ @Getter
+ public enum PurlType {
+ /**
+ * GitHub URL
+ */
+ GITHUB("github", "https://github.com/%s"),
+ /**
+ * Node URL
+ */
+ NPM("npm", "https://www.npmjs.com/package/%s"),
+ /**
+ * Maven Central URL
+ */
+ MAVEN("maven", "https://mvnrepository.com/artifact/%s"),
+ /**
+ * Ruby Gems URL
+ */
+ GEM("gem", "https://rubygems.org/gems/%s"),
+ /**
+ * Python PyPI URL
+ */
+ PYPI("pypi", "https://pypi.org/project/%s"),
+ /**
+ * Golang URL
+ */
+ GOLANG("golang", "https://pkg.go.dev/%s"),
+ /**
+ * MS Nuget URL
+ */
+ NUGET("nuget", "https://www.nuget.org/packages/%s");
+
+ private final String type;
+ private final String urlPattern;
+
+ /**
+ * Setup PURL type/URL enum
+ *
+ * @param type PURL Type
+ * @param urlPattern URL pattern
+ */
+ PurlType(String type, String urlPattern) {
+ this.type = type;
+ this.urlPattern = urlPattern;
+ }
+ }
+
+ /**
+ * Checks if the given PackageURL is supported for conversion.
+ *
+ * @param purl The PackageURL to check
+ * @return true
if the PackageURL can be converted to a browsable URL
+ */
+ public static boolean isSupported(@NonNull PackageURL purl) {
+ try {
+ findPurlType(purl.getType());
+ return true;
+ } catch (RuntimeException e) {
+ log.warn("Failed to find PURL type {} from {}: {}", purl.getType(), purl, e.getLocalizedMessage());
+ }
+ return false;
+ }
+
+ /**
+ * Converts a PackageURL to its browsable web URL.
+ * Returns null if the conversion is not possible.
+ *
+ * @param purl The PackageURL to convert
+ * @return The browsable web URL or null
if conversion fails
+ */
+ @Nullable
+ public static String convert(@NonNull PackageURL purl) {
+ try {
+ PurlType purlType = findPurlType(purl.getType());
+ String nameSpace = purl.getNamespace();
+ String fullName = nameSpace != null ? nameSpace + "/" + purl.getName() : purl.getName();
+ return String.format(purlType.getUrlPattern(), fullName);
+ } catch (RuntimeException e) {
+ log.debug("Failed to convert purl to URL for {}: {}", purl, e.getLocalizedMessage(), e);
+ }
+ return null;
+ }
+
+ /**
+ * Determine if we have a supported PURL Type or not
+ *
+ * @param type PURL type string
+ * @return Supported PURL Type
+ * @throws IllegalArgumentException if type cannot be found on supported list
+ */
+ private static PurlType findPurlType(@NonNull String type) throws IllegalArgumentException {
+ for (PurlType purlType : PurlType.values()) {
+ if (purlType.getType().equals(type)) {
+ return purlType;
+ }
+ }
+ throw new IllegalArgumentException(String.format("Unsupported package type: %s", type));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/scanoss/TestBom.java b/src/test/java/com/scanoss/TestBom.java
new file mode 100644
index 0000000..c6c5828
--- /dev/null
+++ b/src/test/java/com/scanoss/TestBom.java
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss;
+
+import com.scanoss.settings.Bom;
+import com.scanoss.settings.ReplaceRule;
+import com.scanoss.settings.RuleComparator;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+
+@Slf4j
+public class TestBom {
+
+ @Before
+ public void Setup() {
+ log.info("Starting Bom test cases...");
+ }
+
+ @Test
+ public void testReplaceRulesSortingAllCombinations() {
+ log.info("<-- Starting testReplaceRulesSortingAllCombinations");
+
+ // Create rules with different combinations of path and purl
+ ReplaceRule bothFieldsLongPath = ReplaceRule.builder()
+ .path("/very/long/path/to/specific/file.txt")
+ .purl("pkg:maven/org.example/library@1.0.0")
+ .build();
+
+ ReplaceRule bothFieldsShortPath = ReplaceRule.builder()
+ .path("/short/path")
+ .purl("pkg:maven/org.example/another@1.0.0")
+ .build();
+
+ ReplaceRule purlOnly = ReplaceRule.builder()
+ .purl("pkg:maven/org.example/another@2.0.0")
+ .build();
+
+ ReplaceRule pathOnly = ReplaceRule.builder()
+ .path("/another/path")
+ .build();
+
+ // Create Bom with rules in random order
+ Bom bom = Bom.builder()
+ .replace(pathOnly)
+ .replace(bothFieldsShortPath)
+ .replace(purlOnly)
+ .replace(bothFieldsLongPath)
+ .build();
+
+ // Get sorted rules
+ List sortedRules = bom.getReplaceRulesByPriority();
+
+ // Verify order
+ assertEquals("Rule with both fields and longer path should be first", bothFieldsLongPath, sortedRules.get(0));
+ assertEquals("Rule with both fields and shorter path should be second", bothFieldsShortPath, sortedRules.get(1));
+ assertEquals("Rule with purl only should be third", purlOnly, sortedRules.get(2));
+ assertEquals("Rule with path only should be last", pathOnly, sortedRules.get(3));
+
+ log.info("Finished testReplaceRulesSortingAllCombinations -->");
+ }
+
+
+ @Test
+ public void testReplaceRulesSortingWithDuplicatePaths() {
+ log.info("<-- Starting testReplaceRulesSortingWithDuplicatePaths");
+
+ // Create rules with same path length but different paths
+ ReplaceRule pathRule1 = ReplaceRule.builder()
+ .path("/path/to/first")
+ .build();
+
+ ReplaceRule pathRule2 = ReplaceRule.builder()
+ .path("/path/to/other")
+ .build();
+
+ // Create Bom with rules
+ Bom bom = Bom.builder()
+ .replace(pathRule1)
+ .replace(pathRule2)
+ .build();
+
+ // Get sorted rules
+ List sortedRules = bom.getReplaceRulesByPriority();
+
+ // Verify the rules with same path length maintain original order
+ assertEquals("Size should be 2", 2, sortedRules.size());
+ assertTrue("Both rules should have same priority",
+ new RuleComparator().compare(sortedRules.get(0), sortedRules.get(1)) == 0);
+
+ log.info("Finished testReplaceRulesSortingWithDuplicatePaths -->");
+ }
+
+
+ @Test
+ public void testReplaceRulesSortingEmptyList() {
+ log.info("<-- Starting testReplaceRulesSortingEmptyList");
+
+ // Create Bom with no rules
+ Bom bom = Bom.builder().build();
+
+ // Get sorted rules
+ List sortedRules = bom.getReplaceRulesByPriority();
+
+ // Verify
+ assertNotNull("Sorted list should not be null", sortedRules);
+ assertTrue("Sorted list should be empty", sortedRules.isEmpty());
+
+ log.info("Finished testReplaceRulesSortingEmptyList -->");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/scanoss/TestCli.java b/src/test/java/com/scanoss/TestCli.java
index be975dc..166f01e 100644
--- a/src/test/java/com/scanoss/TestCli.java
+++ b/src/test/java/com/scanoss/TestCli.java
@@ -138,9 +138,15 @@ public void TestScanCommandPositive() {
exitCode = new picocli.CommandLine(new CommandLine()).execute(args2);
assertEquals("command should not fail", 0, exitCode);
+
+ String[] args3 = new String[]{"-d", "scan", "src/test/java/com", "--settings", "src/test/resources/scanoss.json"
+ };
+ exitCode = new picocli.CommandLine(new CommandLine()).execute(args3);
+ assertEquals("command should not fail", 0, exitCode);
log.info("Finished {} -->", methodName);
}
+
@Test
public void TestScanCommandNegative() {
String methodName = new Object() {
@@ -168,6 +174,16 @@ public void TestScanCommandNegative() {
exitCode = new picocli.CommandLine(new CommandLine()).execute(args5);
assertTrue("command should fail", exitCode != 0);
+ String[] args6 = new String[]{"-d", "scan", "src/test/java/com", "--settings", "does-not-exist.json"};
+ exitCode = new picocli.CommandLine(new CommandLine()).execute(args6);
+ assertTrue("command should fail", exitCode != 0);
+
+
+ String[] args7 = new String[]{"-d", "scan", "src/test/java/com", "--settings", "src/test/resources/scanoss-broken.json"};
+ exitCode = new picocli.CommandLine(new CommandLine()).execute(args7);
+ assertTrue("command should fail", exitCode != 0);
+
+
log.info("Finished {} -->", methodName);
}
diff --git a/src/test/java/com/scanoss/TestConstants.java b/src/test/java/com/scanoss/TestConstants.java
index 516a9be..9aeb8aa 100644
--- a/src/test/java/com/scanoss/TestConstants.java
+++ b/src/test/java/com/scanoss/TestConstants.java
@@ -165,6 +165,84 @@ public class TestConstants {
" ]\n" +
" }\n" +
" ],\n" +
+ " \"src/spdx.c\": [\n" +
+ " {\n" +
+ " \"component\": \"scanner.c\",\n" +
+ " \"file\": \"scanner.c-1.3.3/src/spdx.c\",\n" +
+ " \"file_hash\": \"00693585177fc51a8d16b2b890f39277\",\n" +
+ " \"file_url\": \"https://api.osskb.org/file_contents/00693585177fc51a8d16b2b890f39277\",\n" +
+ " \"id\": \"snippet\",\n" +
+ " \"latest\": \"1.3.4\",\n" +
+ " \"licenses\": [\n" +
+ " {\n" +
+ " \"checklist_url\": \"https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-1.0-or-later.txt\",\n" +
+ " \"copyleft\": \"yes\",\n" +
+ " \"incompatible_with\": \"Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1\",\n" +
+ " \"name\": \"GPL-1.0-or-later\",\n" +
+ " \"osadl_updated\": \"2024-11-29T15:09:00+0000\",\n" +
+ " \"patent_hints\": \"no\",\n" +
+ " \"source\": \"scancode\",\n" +
+ " \"url\": \"https://spdx.org/licenses/GPL-1.0-or-later.html\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"checklist_url\": \"https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt\",\n" +
+ " \"copyleft\": \"yes\",\n" +
+ " \"incompatible_with\": \"Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1\",\n" +
+ " \"name\": \"GPL-2.0-or-later\",\n" +
+ " \"osadl_updated\": \"2024-11-29T15:09:00+0000\",\n" +
+ " \"patent_hints\": \"yes\",\n" +
+ " \"source\": \"scancode\",\n" +
+ " \"url\": \"https://spdx.org/licenses/GPL-2.0-or-later.html\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"name\": \"CC0-1.0\",\n" +
+ " \"source\": \"scancode\",\n" +
+ " \"url\": \"https://spdx.org/licenses/CC0-1.0.html\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"checklist_url\": \"https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt\",\n" +
+ " \"copyleft\": \"yes\",\n" +
+ " \"incompatible_with\": \"Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1\",\n" +
+ " \"name\": \"GPL-2.0-or-later\",\n" +
+ " \"osadl_updated\": \"2024-11-29T15:09:00+0000\",\n" +
+ " \"patent_hints\": \"yes\",\n" +
+ " \"source\": \"file_spdx_tag\",\n" +
+ " \"url\": \"https://spdx.org/licenses/GPL-2.0-or-later.html\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"checklist_url\": \"https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt\",\n" +
+ " \"copyleft\": \"yes\",\n" +
+ " \"incompatible_with\": \"Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1\",\n" +
+ " \"name\": \"GPL-2.0-only\",\n" +
+ " \"osadl_updated\": \"2024-11-29T15:09:00+0000\",\n" +
+ " \"patent_hints\": \"yes\",\n" +
+ " \"source\": \"component_declared\",\n" +
+ " \"url\": \"https://spdx.org/licenses/GPL-2.0-only.html\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"lines\": \"11-52,81-123\",\n" +
+ " \"matched\": \"63%\",\n" +
+ " \"oss_lines\": \"28-69,28-70\",\n" +
+ " \"purl\": [\n" +
+ " \"pkg:github/scanoss/scanner.c\"\n" +
+ " ],\n" +
+ " \"release_date\": \"2021-05-26\",\n" +
+ " \"server\": {\n" +
+ " \"kb_version\": {\n" +
+ " \"daily\": \"24.12.03\",\n" +
+ " \"monthly\": \"24.11\"\n" +
+ " },\n" +
+ " \"version\": \"5.4.8\"\n" +
+ " },\n" +
+ " \"source_hash\": \"0bcee0405fbf27bc6a9fc6eb8fb58642\",\n" +
+ " \"status\": \"pending\",\n" +
+ " \"url\": \"https://github.com/scanoss/scanner.c\",\n" +
+ " \"url_hash\": \"2d1700ba496453d779d4987255feb5f2\",\n" +
+ " \"url_stats\": {},\n" +
+ " \"vendor\": \"scanoss\",\n" +
+ " \"version\": \"1.3.3\"\n" +
+ " }\n" +
+ " ],\n" +
" \"scanoss/api/__init__.py\": [\n" +
" {\n" +
" \"component\": \"scanoss.py\",\n" +
@@ -256,6 +334,8 @@ public class TestConstants {
" ]\n" +
"}\n";
+
+
// Custom self-signed certificate
static final String customSelfSignedCertificate =
"-----BEGIN CERTIFICATE-----\n" +
diff --git a/src/test/java/com/scanoss/TestJsonUtils.java b/src/test/java/com/scanoss/TestJsonUtils.java
index 00e287c..8f6e7ba 100644
--- a/src/test/java/com/scanoss/TestJsonUtils.java
+++ b/src/test/java/com/scanoss/TestJsonUtils.java
@@ -34,8 +34,7 @@
import java.util.List;
import static com.scanoss.TestConstants.*;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.*;
@Slf4j
public class TestJsonUtils {
@@ -87,4 +86,53 @@ public void TestRawResultsPositive() {
log.info("Finished {} -->", methodName);
}
+
+ @Test
+ public void testToRawJsonStringPositive() {
+ String methodName = new Object() {}.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ JsonObject jsonObject = JsonUtils.toJsonObject(jsonResultsString);
+ List sampleScanResults = JsonUtils.toScanFileResultsFromObject(jsonObject); //TODO: Create sampleScanResults with a helper function
+
+ // Convert to raw JSON strings
+ List rawJsonStrings = JsonUtils.toRawJsonString(sampleScanResults);
+
+ // Verify results
+ assertNotNull("Raw JSON strings should not be null", rawJsonStrings);
+ assertEquals("Should have correct number of results", 4, rawJsonStrings.size());
+
+ // Verify each JSON string can be parsed back to objects
+ for (String jsonString : rawJsonStrings) {
+ JsonObject jObject = JsonUtils.toJsonObject(jsonString);
+ assertNotNull("Parsed JSON object should not be null", jObject);
+ assertEquals("Each JSON object should have exactly one key", 1, jObject.keySet().size());
+ }
+
+ // Verify first result contains expected file path
+ JsonObject firstResult = JsonUtils.toJsonObject(rawJsonStrings.get(0));
+ assertTrue("First result should contain scanoss/__init__.py", firstResult.has("scanoss/__init__.py"));
+
+ JsonObject secondResult = JsonUtils.toJsonObject(rawJsonStrings.get(1));
+ assertTrue("Second result should contain CMSsite/admin/js/npm.js", secondResult.has("CMSsite/admin/js/npm.js"));
+
+ JsonObject thirdResult = JsonUtils.toJsonObject(rawJsonStrings.get(2));
+ assertTrue("Second result should contain src/spdx.c", thirdResult.has("src/spdx.c"));
+
+ JsonObject fourthResult = JsonUtils.toJsonObject(rawJsonStrings.get(3));
+ assertTrue("Second result should contain scanoss/api/__init__.py", fourthResult.has("scanoss/api/__init__.py"));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+
+ @Test
+ public void testToRawJsonStringEmptyList() {
+ List emptyList = new ArrayList<>();
+ List result = JsonUtils.toRawJsonString(emptyList);
+ assertNotNull("Result should not be null for empty input", result);
+ assertTrue("Result should be empty for empty input", result.isEmpty());
+ }
+
+
}
diff --git a/src/test/java/com/scanoss/TestLineRangeUtils.java b/src/test/java/com/scanoss/TestLineRangeUtils.java
new file mode 100644
index 0000000..09007fc
--- /dev/null
+++ b/src/test/java/com/scanoss/TestLineRangeUtils.java
@@ -0,0 +1,177 @@
+package com.scanoss;
+
+import com.scanoss.utils.LineRange;
+import com.scanoss.utils.LineRangeUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+@Slf4j
+public class TestLineRangeUtils {
+
+ @Before
+ public void Setup() {
+ log.info("Starting Line Range Utils test cases...");
+ log.debug("Logging debug enabled");
+ log.trace("Logging trace enabled");
+ }
+
+ @Test
+ public void testSingleRangeOverlap() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ LineRange range1 = new LineRange(1, 10);
+ LineRange range2 = new LineRange(5, 15);
+
+ assertTrue("Overlapping ranges should return true", range1.overlaps(range2));
+ assertTrue("Overlap should be commutative", range2.overlaps(range1));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testNonOverlappingRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ LineRange range1 = new LineRange(1, 5);
+ LineRange range2 = new LineRange(6, 10);
+
+ assertFalse("Non-overlapping ranges should return false", range1.overlaps(range2));
+ assertFalse("Non-overlap should be commutative", range2.overlaps(range1));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testAdjacentRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ LineRange range1 = new LineRange(1, 5);
+ LineRange range2 = new LineRange(5, 10);
+
+ assertTrue("Adjacent ranges should be considered overlapping", range1.overlaps(range2));
+ assertTrue("Adjacent overlap should be commutative", range2.overlaps(range1));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testParseValidLineRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ String rangesStr = "11-52,81-123";
+ List ranges = LineRangeUtils.parseLineRanges(rangesStr);
+
+ assertEquals("Should parse two ranges", 2, ranges.size());
+ assertEquals("First range should start at 11", 11, ranges.get(0).getStart());
+ assertEquals("First range should end at 52", 52, ranges.get(0).getEnd());
+ assertEquals("Second range should start at 81", 81, ranges.get(1).getStart());
+ assertEquals("Second range should end at 123", 123, ranges.get(1).getEnd());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testParseEmptyInput() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ List ranges = LineRangeUtils.parseLineRanges("");
+ assertTrue("Empty input should return empty list", ranges.isEmpty());
+
+ ranges = LineRangeUtils.parseLineRanges(null);
+ assertTrue("Null input should return empty list", ranges.isEmpty());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testParseInvalidFormat() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ List ranges = LineRangeUtils.parseLineRanges("11-52-81");
+ assertTrue("Invalid format should be skipped", ranges.isEmpty());
+
+ ranges = LineRangeUtils.parseLineRanges(",,,");
+ assertTrue("Invalid format should be skipped", ranges.isEmpty());
+
+ ranges = LineRangeUtils.parseLineRanges("abc-def");
+ assertTrue("Non-numeric ranges should be skipped", ranges.isEmpty());
+
+ ranges = LineRangeUtils.parseLineRanges("11-52,invalid,81-123");
+ assertEquals("Should parse valid ranges and skip invalid ones", 2, ranges.size());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testHasOverlappingRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ String rangesStr = "1-10,20-30";
+ int range_start = 8;
+ int range_end = 15;
+
+ List ranges = LineRangeUtils.parseLineRanges(rangesStr);
+ LineRange range = new LineRange(range_start, range_end);
+
+ assertTrue("Should detect overlapping ranges",
+ LineRangeUtils.hasOverlappingRanges(ranges, range));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testNoOverlappingRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ String rangesStr = "1-10,20-30";
+ int range_start = 11;
+ int range_end = 15;
+
+ List ranges = LineRangeUtils.parseLineRanges(rangesStr);
+ LineRange range = new LineRange(range_start, range_end);
+
+ assertFalse("Should not detect overlapping ranges",
+ LineRangeUtils.hasOverlappingRanges(ranges, range));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testSingleLineRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ String rangesStr = "5-5,10-10";
+ List ranges = LineRangeUtils.parseLineRanges(rangesStr);
+
+ assertEquals("Should parse two single-line ranges", 2, ranges.size());
+ assertEquals("First range should be single line",
+ ranges.get(0).getStart(), ranges.get(0).getEnd());
+ assertEquals("Second range should be single line",
+ ranges.get(1).getStart(), ranges.get(1).getEnd());
+
+ log.info("Finished {} -->", methodName);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/scanoss/TestPurl2Url.java b/src/test/java/com/scanoss/TestPurl2Url.java
new file mode 100644
index 0000000..32ecc17
--- /dev/null
+++ b/src/test/java/com/scanoss/TestPurl2Url.java
@@ -0,0 +1,350 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2023, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.scanoss;
+import com.github.packageurl.PackageURL;
+import com.scanoss.utils.Purl2Url;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+@Slf4j
+public class TestPurl2Url {
+
+ @Test
+ public void testValidGithubUrl() throws Exception {
+ log.info("<-- Starting testValidGithubUrl");
+
+ // Create test data - using React repository
+ PackageURL purl = new PackageURL("pkg:github/facebook/react");
+
+ // Verify support
+ assertTrue("React GitHub repo should be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull("Generated URL should not be null", url);
+ assertEquals("URL should match React GitHub repo",
+ "https://github.com/facebook/react", url);
+
+ log.info("Finished testValidGithubUrl -->");
+ }
+
+ @Test
+ public void testValidNpmUrl() throws Exception {
+ log.info("<-- Starting testValidNpmUrl");
+
+ // Create test data - using Express.js
+ PackageURL purl = new PackageURL("pkg:npm/express");
+
+ // Verify support
+ assertTrue("Express npm package should be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull("Generated URL should not be null", url);
+ assertEquals("URL should match Express npm package",
+ "https://www.npmjs.com/package/express", url);
+
+ log.info("Finished testValidNpmUrl -->");
+ }
+
+ @Test
+ public void testScopedNpmPackage() throws Exception {
+ log.info("<-- Starting testScopedNpmPackage");
+
+ // Create test data - using Angular core package
+ PackageURL purl = new PackageURL("pkg:npm/%40angular/core");
+
+ // Verify support
+ assertTrue("Angular core package should be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull("Generated URL should not be null", url);
+ assertEquals("URL should match Angular core package",
+ "https://www.npmjs.com/package/@angular/core", url);
+
+ log.info("Finished testScopedNpmPackage -->");
+ }
+
+ @Test
+ public void testValidMavenUrl() throws Exception {
+ log.info("<-- Starting testValidMavenUrl");
+
+ // Create test data - using Spring Boot
+ PackageURL purl = new PackageURL("pkg:maven/org.springframework.boot/spring-boot");
+
+ // Verify support
+ assertTrue("Spring Boot Maven artifact should be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull("Generated URL should not be null", url);
+ assertEquals("URL should match Spring Boot Maven artifact",
+ "https://mvnrepository.com/artifact/org.springframework.boot/spring-boot", url);
+
+ log.info("Finished testValidMavenUrl -->");
+ }
+
+ @Test
+ public void testValidPypiUrl() throws Exception {
+ log.info("<-- Starting testValidPypiUrl");
+
+ // Create test data - using Django
+ PackageURL purl = new PackageURL("pkg:pypi/django");
+
+ // Verify support
+ assertTrue("Django PyPI package should be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull("Generated URL should not be null", url);
+ assertEquals("URL should match Django PyPI package",
+ "https://pypi.org/project/django", url);
+
+ log.info("Finished testValidPypiUrl -->");
+ }
+
+ @Test
+ public void testValidNugetUrl() throws Exception {
+ log.info("<-- Starting testValidNugetUrl");
+
+ // Create test data - using Newtonsoft.Json
+ PackageURL purl = new PackageURL("pkg:nuget/Newtonsoft.Json");
+
+ // Verify support
+ assertTrue("Newtonsoft.Json NuGet package should be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull("Generated URL should not be null", url);
+ assertEquals("URL should match Newtonsoft.Json NuGet package",
+ "https://www.nuget.org/packages/Newtonsoft.Json", url);
+
+ log.info("Finished testValidNugetUrl -->");
+ }
+
+ @Test
+ public void testUnsupportedType() throws Exception {
+ log.info("<-- Starting testUnsupportedType");
+
+ // Create test data - using an unsupported type
+ PackageURL purl = new PackageURL("pkg:unknown/test-package");
+
+ // Verify non-support
+ assertFalse("Unknown package type should not be supported", Purl2Url.isSupported(purl));
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNull("Generated URL should be null for unsupported type", url);
+
+ log.info("Finished testUnsupportedType -->");
+ }
+
+ @Test
+ public void testAllPackageTypes() throws Exception {
+ log.info("<-- Starting testAllPackageTypes");
+
+ class PackageTestCase {
+ final String purl;
+ final String expectedUrl;
+ final String description;
+
+ PackageTestCase(String purl, String expectedUrl, String description) {
+ this.purl = purl;
+ this.expectedUrl = expectedUrl;
+ this.description = description;
+ }
+ }
+
+ List testCases = Arrays.asList(
+ // GitHub Cases
+ new PackageTestCase(
+ "pkg:github/facebook/react",
+ "https://github.com/facebook/react",
+ "Standard GitHub repository"
+ ),
+ new PackageTestCase(
+ "pkg:github/apache/kafka",
+ "https://github.com/apache/kafka",
+ "GitHub repo with no special chars"
+ ),
+ new PackageTestCase(
+ "pkg:github/spring-projects/spring-boot",
+ "https://github.com/spring-projects/spring-boot",
+ "GitHub repo with hyphen"
+ ),
+
+ // NPM Cases
+ new PackageTestCase(
+ "pkg:npm/express",
+ "https://www.npmjs.com/package/express",
+ "Simple NPM package"
+ ),
+ new PackageTestCase(
+ "pkg:npm/%40angular/core",
+ "https://www.npmjs.com/package/@angular/core",
+ "Scoped NPM package"
+ ),
+ new PackageTestCase(
+ "pkg:npm/%40types/node",
+ "https://www.npmjs.com/package/@types/node",
+ "TypeScript definitions package"
+ ),
+
+ // Maven Cases
+ new PackageTestCase(
+ "pkg:maven/org.springframework.boot/spring-boot",
+ "https://mvnrepository.com/artifact/org.springframework.boot/spring-boot",
+ "Standard Maven artifact"
+ ),
+ new PackageTestCase(
+ "pkg:maven/com.google.guava/guava",
+ "https://mvnrepository.com/artifact/com.google.guava/guava",
+ "Google Guava Maven artifact"
+ ),
+ new PackageTestCase(
+ "pkg:maven/io.quarkus/quarkus-core",
+ "https://mvnrepository.com/artifact/io.quarkus/quarkus-core",
+ "Quarkus Maven artifact"
+ ),
+
+ // PyPI Cases
+ new PackageTestCase(
+ "pkg:pypi/requests",
+ "https://pypi.org/project/requests",
+ "Simple PyPI package"
+ ),
+ new PackageTestCase(
+ "pkg:pypi/django",
+ "https://pypi.org/project/django",
+ "Django Python package"
+ ),
+ new PackageTestCase(
+ "pkg:pypi/python-dateutil",
+ "https://pypi.org/project/python-dateutil",
+ "PyPI package with hyphen"
+ ),
+
+ // Gem Cases
+ new PackageTestCase(
+ "pkg:gem/rails",
+ "https://rubygems.org/gems/rails",
+ "Simple Ruby gem"
+ ),
+ new PackageTestCase(
+ "pkg:gem/activerecord",
+ "https://rubygems.org/gems/activerecord",
+ "ActiveRecord Ruby gem"
+ ),
+ new PackageTestCase(
+ "pkg:gem/devise",
+ "https://rubygems.org/gems/devise",
+ "Devise authentication gem"
+ ),
+
+ // Golang Cases
+ new PackageTestCase(
+ "pkg:golang/golang.org/x/text",
+ "https://pkg.go.dev/golang.org/x/text",
+ "Official Go package"
+ ),
+ new PackageTestCase(
+ "pkg:golang/github.com/gin-gonic/gin",
+ "https://pkg.go.dev/github.com/gin-gonic/gin",
+ "Gin web framework"
+ ),
+ new PackageTestCase(
+ "pkg:golang/google.golang.org/grpc",
+ "https://pkg.go.dev/google.golang.org/grpc",
+ "gRPC Go package"
+ ),
+
+ // NuGet Cases
+ new PackageTestCase(
+ "pkg:nuget/Newtonsoft.Json",
+ "https://www.nuget.org/packages/Newtonsoft.Json",
+ "Popular JSON library"
+ ),
+ new PackageTestCase(
+ "pkg:nuget/Microsoft.AspNetCore.App",
+ "https://www.nuget.org/packages/Microsoft.AspNetCore.App",
+ "Microsoft ASP.NET Core package"
+ ),
+ new PackageTestCase(
+ "pkg:nuget/Serilog.Sinks.Console",
+ "https://www.nuget.org/packages/Serilog.Sinks.Console",
+ "NuGet package with dots"
+ )
+ );
+
+ // Run all test cases
+ for (PackageTestCase tc : testCases) {
+ log.info("Testing: {}", tc.description);
+
+ PackageURL purl = new PackageURL(tc.purl);
+
+ // Verify support
+ assertTrue(
+ String.format("%s should be supported", tc.description),
+ Purl2Url.isSupported(purl)
+ );
+
+ // Generate URL
+ String url = Purl2Url.convert(purl);
+
+ // Verify
+ assertNotNull(
+ String.format("Generated URL should not be null for %s", tc.description),
+ url
+ );
+ assertEquals(
+ String.format("URL should match expected for %s", tc.description),
+ tc.expectedUrl,
+ url
+ );
+ }
+
+ log.info("Finished testAllPackageTypes -->");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/scanoss/TestScannerPostProcessor.java b/src/test/java/com/scanoss/TestScannerPostProcessor.java
new file mode 100644
index 0000000..5cd8015
--- /dev/null
+++ b/src/test/java/com/scanoss/TestScannerPostProcessor.java
@@ -0,0 +1,463 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss;
+
+import com.google.gson.JsonObject;
+import com.scanoss.dto.ScanFileResult;
+import com.scanoss.settings.Bom;
+import com.scanoss.settings.RemoveRule;
+import com.scanoss.settings.ReplaceRule;
+import com.scanoss.utils.JsonUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static com.scanoss.TestConstants.jsonResultsString;
+import static org.junit.Assert.*;
+
+@Slf4j
+public class TestScannerPostProcessor {
+ private ScannerPostProcessor scannerPostProcessor;
+ private List sampleScanResults;
+ private List longScanResults;
+
+ @Before
+ public void Setup() throws URISyntaxException, IOException {
+ log.info("Starting ScannerPostProcessor test cases...");
+ scannerPostProcessor = ScannerPostProcessor.builder().build();
+ JsonObject jsonObject = JsonUtils.toJsonObject(jsonResultsString);
+ sampleScanResults = JsonUtils.toScanFileResultsFromObject(jsonObject); //TODO: Create sampleScanResults with a helper function
+
+
+ var resource = getClass().getClassLoader().getResource("results.json");
+ if (resource == null) {
+ throw new IllegalStateException(
+ "Required test resource 'results.json' not found. Please ensure it exists in src/test/resources/data/"
+ );
+ }
+
+
+ String json = Files.readString(Paths.get(resource.toURI()), StandardCharsets.UTF_8);
+ longScanResults = JsonUtils.toScanFileResultsFromObject(JsonUtils.toJsonObject(json));
+
+ }
+
+
+ /**
+ * TESTING REMOVE RULES
+ **/
+ @Test
+ public void TestRemoveRuleWithPathAndPurl() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ RemoveRule rule = RemoveRule.builder()
+ .purl("pkg:github/twbs/bootstrap")
+ .path("CMSsite/admin/js/npm.js")
+ .build();
+
+ Bom bom = Bom.builder().remove(rule).build();
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ // Verify
+ assertEquals("Should have one result less after removal", sampleScanResults.size() - 1, results.size());
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void TestRemoveRuleWithPurlOnly() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ // Setup remove rule with only purl
+ RemoveRule removeRule = RemoveRule.builder()
+ .purl("pkg:npm/mip-bootstrap")
+ .build();
+
+ Bom bom = Bom.builder().
+ remove(Collections.singletonList(removeRule))
+ .build();
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ // Verify
+ assertEquals("Size should decrease by 1 after removal",
+ sampleScanResults.size() - 1,
+ results.size());
+
+ assertFalse("Should remove file CMSsite/admin/js/npm.js",
+ results.stream().anyMatch(r -> r.getFilePath().matches("CMSsite/admin/js/npm.js")));
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void TestNoMatchingRemoveRules() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+
+ // Setup non-matching remove rule
+ RemoveRule removeRule = RemoveRule.builder()
+ .purl("pkg:github/non-existing/lib@1.0.0")
+ .path("non/existing/path.c")
+ .build();
+
+ Bom bom = Bom.builder().
+ remove(Collections.singletonList(removeRule))
+ .build();
+
+
+ // Process results
+ List results = scannerPostProcessor.process(longScanResults, bom);
+
+ // Verify
+ assertEquals("Should keep all results", longScanResults.size(), results.size());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void TestMultipleRemoveRules() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ // Setup multiple remove rules
+ Bom bom = Bom.builder().
+ remove(Arrays.asList(
+ RemoveRule.builder()
+ .purl("pkg:npm/myoneui")
+ .path("CMSsite/admin/js/npm.js")
+ .build(),
+
+ RemoveRule.builder()
+ .purl("pkg:pypi/scanoss")
+ .build(),
+
+ RemoveRule.builder()
+ .path("scanoss/__init__.py")
+ .build(),
+
+ RemoveRule.builder()
+ .path("src/spdx.c")
+ .build()
+ ))
+ .build();
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ // Verify
+ assertFalse("Should keep scanoss/__init__.py since it's a non match", results.isEmpty());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void TestEmptyRemoveRules() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ Bom bom = Bom.builder()
+ .build();
+
+ // Process results with empty remove rules
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ // Verify
+ assertEquals("Should keep all results", sampleScanResults.size(), results.size());
+ assertEquals("Results should match original", sampleScanResults, results);
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testRemoveRuleWithNonOverlappingLineRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ // Setup remove rule with non-overlapping line ranges
+ Bom bom = Bom.builder()
+ .remove(Collections.singletonList(
+ RemoveRule.builder()
+ .path("src/spdx.c")
+ .startLine(1)
+ .endLine(10) // Before the first range
+ .build()
+ ))
+ .build();
+
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ // Verify - should keep because lines don't overlap
+ assertEquals("Results should match original", sampleScanResults.size(), results.size());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testRemoveRuleWithOverlappingLineRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ // Setup remove rule with overlapping line ranges
+ Bom bom = Bom.builder()
+ .remove(Collections.singletonList(
+ RemoveRule.builder()
+ .path("src/spdx.c")
+ .startLine(40)
+ .endLine(60) // Overlaps with 11-52
+ .build()
+ ))
+ .build();
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ // Verify - should remove because lines overlap
+ assertEquals("Should have one result less after removal", sampleScanResults.size() - 1, results.size());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testMultipleRemoveRulesWithMixedLineRanges() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ // Setup multiple remove rules with different line range configurations
+ Bom bom = Bom.builder()
+ .remove(Arrays.asList(
+ RemoveRule.builder()
+ .path("src/spdx.c")
+ .startLine(1)
+ .endLine(10) // Non-overlapping
+ .build(),
+ RemoveRule.builder()
+ .path("src/spdx.c")
+ .startLine(40)
+ .endLine(60) // Overlapping
+ .build()
+ ))
+ .build();
+
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ assertEquals("Should have one result less after removal", sampleScanResults.size() - 1, results.size());
+
+ log.info("Finished {} -->", methodName);
+ }
+
+
+ /**
+ * TESTING REPLACE RULES
+ **/
+ @Test
+ public void TestReplaceRuleWithEmptyPurl() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ // Setup replace rule with empty PURL
+ ReplaceRule replace = ReplaceRule.builder()
+ .purl("pkg:github/scanoss/scanoss.py")
+ .replaceWith("")
+ .build();
+
+ Bom bom = Bom.builder()
+ .replace(Collections.singletonList(replace))
+ .build();
+
+
+ // Find the specific result for scanoss.py
+ Optional originalResult = sampleScanResults.stream()
+ .filter(r -> r.getFilePath().equals("scanoss/api/__init__.py"))
+ .findFirst();
+
+ assertTrue("Original result should exist", originalResult.isPresent());
+ String originalPurl = originalResult.get().getFileDetails().get(0).getPurls()[0];
+
+
+ // Process results
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ Optional processedResult = results.stream()
+ .filter(r -> r.getFilePath().equals("scanoss/api/__init__.py"))
+ .findFirst();
+
+ assertTrue("Processed result should exist", processedResult.isPresent());
+
+ // Verify original PURL remains unchanged
+ String resultPurl = processedResult.get().getFileDetails().get(0).getPurls()[0];
+ assertEquals("PURL should remain unchanged with empty replacement", originalPurl, resultPurl);
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test()
+ public void TestReplaceRuleWithPurl() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+
+ // Setup replace rule with empty PURL
+ ReplaceRule replace = ReplaceRule.builder()
+ .purl("pkg:github/scanoss/scanoss.py")
+ .replaceWith("pkg:github/scanoss/scanner.c")
+ .build();
+
+ Bom bom = Bom.builder()
+ .replace(Collections.singletonList(replace))
+ .build();
+
+
+ List results = scannerPostProcessor.process(sampleScanResults, bom);
+
+ Optional processedResult = results.stream()
+ .filter(r -> r.getFilePath().equals("scanoss/api/__init__.py"))
+ .findFirst();
+
+ assertTrue("Processed result should exist", processedResult.isPresent());
+
+ // Verify exactly one PURL exists and it's the correct one
+ String[] processedPurls = processedResult.get().getFileDetails().get(0).getPurls();
+ assertEquals("Should have exactly one PURL", 1, processedPurls.length);
+ assertEquals("PURL should be scanner.c",
+ "pkg:github/scanoss/scanner.c", processedPurls[0]);
+
+ log.info("Finished {} -->", methodName);
+
+ }
+
+
+ @Test()
+ public void TestOriginalPurlExistsWhenNoReplacementRule() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ String originalPurl = "pkg:github/scanoss/scanner.c";
+
+ // Setup BOM without replace rule - expecting original PURL to exist
+ Bom bom = Bom.builder()
+ .build();
+
+ List results = scannerPostProcessor.process(longScanResults, bom);
+
+ assertNotNull("Results should not be null", results);
+ assertFalse("Results should not be empty", results.isEmpty());
+
+ List allPurls = results.stream()
+ .map(result -> result.getFileDetails().get(0).getPurls())
+ .flatMap(Arrays::stream)
+ .collect(Collectors.toList());
+
+ log.info("All PURLs found: {}", allPurls);
+ log.info("Original PURL we're looking for: '{}'", originalPurl);
+
+
+ boolean hasOriginalPurl = results.stream()
+ .map(result -> result.getFileDetails().get(0).getPurls())
+ .flatMap(Arrays::stream)
+ .anyMatch(purl -> {
+ log.info("Comparing: '{}' with '{}' = {}",
+ purl, originalPurl, purl.equals(originalPurl));
+ return purl.equals(originalPurl);
+ });
+
+ assertTrue("Original PURL should exist since no replacement rule was set", hasOriginalPurl);
+
+ log.info("Finished {} -->", methodName);
+ }
+
+
+ @Test
+ public void TestOriginalPurlNotExistsWhenReplacementRuleDefined() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ String originalPurl = "pkg:github/scanoss/scanner.c";
+ String replacementPurl = "pkg:maven/com.scanoss/scanoss";
+
+ // Setup replace rule
+ ReplaceRule replace = ReplaceRule.builder()
+ .purl(originalPurl)
+ .replaceWith(replacementPurl)
+ .build();
+
+ Bom bom = Bom.builder()
+ .replace(Collections.singletonList(replace))
+ .build();
+
+ List results = scannerPostProcessor.process(longScanResults, bom);
+
+ assertNotNull("Results should not be null", results);
+ assertFalse("Results should not be empty", results.isEmpty());
+
+ boolean hasOriginalPurl = results.stream()
+ .map(result -> result.getFileDetails().get(0).getPurls())
+ .flatMap(Arrays::stream)
+ .anyMatch(purl -> purl.equals(originalPurl));
+
+ assertFalse("Original PURL should not exist when replacement rule is set", hasOriginalPurl);
+
+ boolean hasReplacementPurl = results.stream()
+ .map(result -> result.getFileDetails().get(0).getPurls())
+ .flatMap(Arrays::stream)
+ .anyMatch(purl -> purl.equals(replacementPurl));
+
+ assertTrue("Replacement PURL should exist", hasReplacementPurl);
+
+ log.info("Finished {} -->", methodName);
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/scanoss/TestSettings.java b/src/test/java/com/scanoss/TestSettings.java
new file mode 100644
index 0000000..8be98ac
--- /dev/null
+++ b/src/test/java/com/scanoss/TestSettings.java
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2024, SCANOSS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scanoss;
+
+import com.scanoss.settings.Settings;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.*;
+
+@Slf4j
+public class TestSettings {
+ private Path existingSettingsPath;
+ private Path nonExistentSettingsPath;
+
+ @Before
+ public void Setup() throws URISyntaxException {
+ log.info("Starting Settings test cases...");
+ log.debug("Logging debug enabled");
+ log.trace("Logging trace enabled");
+
+ // Check if resource exists before attempting to get its path
+ var resource = getClass().getClassLoader().getResource("scanoss.json");
+ if (resource == null) {
+ throw new IllegalStateException(
+ "Required test resource 'scanoss.json' not found. Please ensure it exists in src/test/resources/data/"
+ );
+ }
+
+ existingSettingsPath = Paths.get(resource.toURI());
+ nonExistentSettingsPath = Paths.get("non-existent-settings.json");
+
+ // Verify the file actually exists
+ if (!Files.exists(existingSettingsPath)) {
+ throw new IllegalStateException(
+ "Test file exists as resource but cannot be accessed at path: " +
+ existingSettingsPath
+ );
+ }
+ }
+
+
+ @Test
+ public void testSettingsFromExistingFile() {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+
+ Settings settings = Settings.createFromPath(existingSettingsPath);
+ assertNotNull("Settings should not be null", settings);
+
+ assertEquals("scanner.c", settings.getBom().getRemove().get(0).getPath());
+ assertEquals("pkg:github/scanoss/scanner.c", settings.getBom().getRemove().get(0).getPurl());
+
+
+
+ log.info("Finished {} -->", methodName);
+ }
+
+ @Test
+ public void testSettingsFromNonExistentFile() throws IOException {
+ String methodName = new Object() {
+ }.getClass().getEnclosingMethod().getName();
+ log.info("<-- Starting {}", methodName);
+
+ Settings settings = Settings.createFromPath(nonExistentSettingsPath);
+
+ assertNull("Settings should be null", settings);
+
+ log.info("Finished {} -->", methodName);
+ }
+
+}
diff --git a/src/test/resources/results.json b/src/test/resources/results.json
new file mode 100644
index 0000000..75c2f98
--- /dev/null
+++ b/src/test/resources/results.json
@@ -0,0 +1,392 @@
+{
+ "external/inc/json.h": [
+ {
+ "component": "scanner.c",
+ "file": "scanner.c-1.3.3/external/inc/json.h",
+ "file_hash": "e91a03b850651dd56dd979ba92668a19",
+ "file_url": "https://api.osskb.org/file_contents/e91a03b850651dd56dd979ba92668a19",
+ "id": "file",
+ "latest": "1.3.4",
+ "licenses": [
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt",
+ "copyleft": "no",
+ "name": "BSD-2-Clause",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "file_header",
+ "url": "https://spdx.org/licenses/BSD-2-Clause.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt",
+ "copyleft": "no",
+ "name": "BSD-2-Clause",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/BSD-2-Clause.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-only",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "component_declared",
+ "url": "https://spdx.org/licenses/GPL-2.0-only.html"
+ }
+ ],
+ "lines": "all",
+ "matched": "100%",
+ "oss_lines": "all",
+ "purl": [
+ "pkg:github/scanoss/scanner.c"
+ ],
+ "release_date": "2021-05-26",
+ "server": {
+ "kb_version": {
+ "daily": "24.11.13",
+ "monthly": "24.10"
+ },
+ "version": "5.4.8"
+ },
+ "source_hash": "e91a03b850651dd56dd979ba92668a19",
+ "status": "pending",
+ "url": "https://github.com/scanoss/scanner.c",
+ "url_hash": "2d1700ba496453d779d4987255feb5f2",
+ "url_stats": {},
+ "vendor": "scanoss",
+ "version": "1.3.3"
+ }
+ ],
+ "src/cyclonedx.c": [
+ {
+ "component": "scanner.c",
+ "file": "scanner.c-1.3.3/src/cyclonedx.c",
+ "file_hash": "342bab2935f4817281eb262c23a4bdd9",
+ "file_url": "https://api.osskb.org/file_contents/342bab2935f4817281eb262c23a4bdd9",
+ "id": "snippet",
+ "latest": "1.3.3",
+ "licenses": [
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-1.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-1.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/GPL-1.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "file_spdx_tag",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-only",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "component_declared",
+ "url": "https://spdx.org/licenses/GPL-2.0-only.html"
+ }
+ ],
+ "lines": "9-9,32-96",
+ "matched": "66%",
+ "oss_lines": "7-7,29-93",
+ "purl": [
+ "pkg:github/scanoss/scanner.c"
+ ],
+ "release_date": "2021-05-26",
+ "server": {
+ "kb_version": {
+ "daily": "24.11.13",
+ "monthly": "24.10"
+ },
+ "version": "5.4.8"
+ },
+ "source_hash": "33bbeaa1f27d48d11a6b81e0d7292562",
+ "status": "pending",
+ "url": "https://github.com/scanoss/scanner.c",
+ "url_hash": "2d1700ba496453d779d4987255feb5f2",
+ "url_stats": {},
+ "vendor": "scanoss",
+ "version": "1.3.3"
+ }
+ ],
+ "src/format_utils.c": [
+ {
+ "component": "scanner.c",
+ "file": "scanner.c-1.3.3/src/format_utils.c",
+ "file_hash": "2691bb31301bbc70edbe7960673b7ca7",
+ "file_url": "https://api.osskb.org/file_contents/2691bb31301bbc70edbe7960673b7ca7",
+ "id": "snippet",
+ "latest": "1.3.3",
+ "licenses": [
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-only",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "component_declared",
+ "url": "https://spdx.org/licenses/GPL-2.0-only.html"
+ }
+ ],
+ "lines": "8-18,41-294",
+ "matched": "84%",
+ "oss_lines": "8-18,48-301",
+ "purl": [
+ "pkg:github/scanoss/scanner.c"
+ ],
+ "release_date": "2021-05-26",
+ "server": {
+ "kb_version": {
+ "daily": "24.11.13",
+ "monthly": "24.10"
+ },
+ "version": "5.4.8"
+ },
+ "source_hash": "1b07d074ce3d81ca10990baed612d5cf",
+ "status": "pending",
+ "url": "https://github.com/scanoss/scanner.c",
+ "url_hash": "2d1700ba496453d779d4987255feb5f2",
+ "url_stats": {},
+ "vendor": "scanoss",
+ "version": "1.3.3"
+ }
+ ],
+ "src/main.c": [
+ {
+ "component": "scanner.c",
+ "file": "scanner.c-1.3.2/src/main.c",
+ "file_hash": "30d93e53539a3ca8db58e8f852fc1c30",
+ "file_url": "https://api.osskb.org/file_contents/30d93e53539a3ca8db58e8f852fc1c30",
+ "id": "snippet",
+ "latest": "1.3.2",
+ "licenses": [
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-1.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-1.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/GPL-1.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "file_spdx_tag",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-only",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "component_declared",
+ "url": "https://spdx.org/licenses/GPL-2.0-only.html"
+ }
+ ],
+ "lines": "7-9,50-84,102-161",
+ "matched": "49%",
+ "oss_lines": "7-9,45-79,96-155",
+ "purl": [
+ "pkg:github/scanoss/scanner.c"
+ ],
+ "release_date": "2021-05-17",
+ "server": {
+ "kb_version": {
+ "daily": "24.11.13",
+ "monthly": "24.10"
+ },
+ "version": "5.4.8"
+ },
+ "source_hash": "54cbf9c7b4ec540b6b9dd08f1c45dd47",
+ "status": "pending",
+ "url": "https://github.com/scanoss/scanner.c",
+ "url_hash": "bcb91cab3d56bc463bb857a257289f1f",
+ "url_stats": {},
+ "vendor": "scanoss",
+ "version": "1.3.2"
+ }
+ ],
+ "src/scanner.c": [
+ {
+ "component": "scanoss.java",
+ "file": "testing/data/test-folder-ignore/test-no-ignore/scanner.c",
+ "file_hash": "f83589f71d6c31a2afb8d374953292f1",
+ "file_url": "https://api.osskb.org/file_contents/f83589f71d6c31a2afb8d374953292f1",
+ "id": "file",
+ "latest": "8598c47",
+ "licenses": [
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt",
+ "copyleft": "no",
+ "name": "MIT",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "component_declared",
+ "url": "https://spdx.org/licenses/MIT.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "file_spdx_tag",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt",
+ "copyleft": "no",
+ "name": "MIT",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "license_file",
+ "url": "https://spdx.org/licenses/MIT.html"
+ }
+ ],
+ "lines": "all",
+ "matched": "100%",
+ "oss_lines": "all",
+ "purl": [
+ "pkg:github/scanoss/scanoss.java",
+ "pkg:maven/com.scanoss/scanoss"
+ ],
+ "release_date": "2024-04-12",
+ "server": {
+ "kb_version": {
+ "daily": "24.11.13",
+ "monthly": "24.10"
+ },
+ "version": "5.4.8"
+ },
+ "source_hash": "f83589f71d6c31a2afb8d374953292f1",
+ "status": "pending",
+ "url": "https://github.com/scanoss/scanoss.java",
+ "url_hash": "7197978c914a0cb767cad47aaa1c8276",
+ "url_stats": {},
+ "vendor": "scanoss",
+ "version": "0.7.1"
+ }
+ ],
+ "src/spdx.c": [
+ {
+ "component": "scanner.c",
+ "file": "scanner.c-1.3.3/src/spdx.c",
+ "file_hash": "00693585177fc51a8d16b2b890f39277",
+ "file_url": "https://api.osskb.org/file_contents/00693585177fc51a8d16b2b890f39277",
+ "id": "snippet",
+ "latest": "1.3.4",
+ "licenses": [
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-1.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-1.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "no",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/GPL-1.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "name": "CC0-1.0",
+ "source": "scancode",
+ "url": "https://spdx.org/licenses/CC0-1.0.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-or-later.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-or-later",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "file_spdx_tag",
+ "url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
+ },
+ {
+ "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt",
+ "copyleft": "yes",
+ "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
+ "name": "GPL-2.0-only",
+ "osadl_updated": "2024-11-04T13:45:00+0000",
+ "patent_hints": "yes",
+ "source": "component_declared",
+ "url": "https://spdx.org/licenses/GPL-2.0-only.html"
+ }
+ ],
+ "lines": "30-79",
+ "matched": "62%",
+ "oss_lines": "28-77",
+ "purl": [
+ "pkg:github/scanoss/scanner.c"
+ ],
+ "release_date": "2021-05-26",
+ "server": {
+ "kb_version": {
+ "daily": "24.11.13",
+ "monthly": "24.10"
+ },
+ "version": "5.4.8"
+ },
+ "source_hash": "920066098c63d986a663132d9ec73e03",
+ "status": "pending",
+ "url": "https://github.com/scanoss/scanner.c",
+ "url_hash": "2d1700ba496453d779d4987255feb5f2",
+ "url_stats": {},
+ "vendor": "scanoss",
+ "version": "1.3.3"
+ }
+ ]
+}
diff --git a/src/test/resources/scanoss.json b/src/test/resources/scanoss.json
new file mode 100644
index 0000000..2d65680
--- /dev/null
+++ b/src/test/resources/scanoss.json
@@ -0,0 +1,12 @@
+{
+ "bom": {
+ "include": [],
+ "remove": [
+ {
+ "path": "scanner.c",
+ "purl": "pkg:github/scanoss/scanner.c"
+ }
+ ]
+ }
+}
+