diff --git a/pom.xml b/pom.xml index 57ad66f..6b075ff 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.scanoss scanoss - 0.7.3 + 0.8.0 jar scanoss.java https://github.com/scanoss/scanoss.java @@ -38,7 +38,7 @@ 11 11 UTF-8 - 2.0.7 + 2.0.16 0.9.13 com.scanoss.cli.CommandLine @@ -77,7 +77,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.36 true @@ -93,7 +93,7 @@ commons-codec commons-codec - 1.16.0 + 1.17.1 org.slf4j @@ -108,19 +108,24 @@ org.apache.tika tika-core - 2.8.0 + 2.9.2 info.picocli picocli - 4.7.4 + 4.7.6 com.google.code.gson gson - 2.10.1 + 2.11.0 compile + + com.github.package-url + packageurl-java + 1.5.0 + @@ -148,9 +153,10 @@ org.apache.maven.plugins maven-assembly-plugin - 3.6.0 + 3.7.1 + with-all-dependencies package single @@ -166,6 +172,40 @@ + + without-slf4j + package + + single + + + + + ${exec.mainClass} + + + + + with-dependencies-excluded-slf4j + + jar + + false + + + / + true + true + + org.slf4j:slf4j-api + org.slf4j:slf4j-simple + + + + + + + diff --git a/src/main/java/com/scanoss/Scanner.java b/src/main/java/com/scanoss/Scanner.java index c859228..b879db3 100644 --- a/src/main/java/com/scanoss/Scanner.java +++ b/src/main/java/com/scanoss/Scanner.java @@ -22,12 +22,15 @@ */ package com.scanoss; +import com.scanoss.dto.ScanFileResult; import com.scanoss.exceptions.ScannerException; import com.scanoss.exceptions.WinnowingException; import com.scanoss.processor.FileProcessor; import com.scanoss.processor.ScanFileProcessor; import com.scanoss.processor.WfpFileProcessor; import com.scanoss.rest.ScanApi; +import com.scanoss.settings.Settings; +import com.scanoss.utils.JsonUtils; import lombok.Builder; import lombok.Getter; import lombok.NonNull; @@ -89,6 +92,8 @@ public class Scanner { private ScanApi scanApi; private ScanFileProcessor scanFileProcessor; private WfpFileProcessor wfpFileProcessor; + private Settings settings; + private ScannerPostProcessor postProcessor; @SuppressWarnings("unused") private Scanner(Boolean skipSnippets, Boolean allExtensions, Boolean obfuscate, Boolean hpsm, @@ -96,7 +101,8 @@ private Scanner(Boolean skipSnippets, Boolean allExtensions, Boolean obfuscate, Integer retryLimit, String url, String apiKey, String scanFlags, String sbomType, String sbom, Integer snippetLimit, String customCert, Proxy proxy, Winnowing winnowing, ScanApi scanApi, - ScanFileProcessor scanFileProcessor, WfpFileProcessor wfpFileProcessor + ScanFileProcessor scanFileProcessor, WfpFileProcessor wfpFileProcessor, + Settings settings, ScannerPostProcessor postProcessor ) { this.skipSnippets = skipSnippets; this.allExtensions = allExtensions; @@ -128,7 +134,9 @@ private Scanner(Boolean skipSnippets, Boolean allExtensions, Boolean obfuscate, this.wfpFileProcessor = Objects.requireNonNullElseGet(wfpFileProcessor, () -> WfpFileProcessor.builder() .winnowing(this.winnowing) .build()); - } + this.settings = Objects.requireNonNullElseGet(settings, () -> Settings.builder().build()); + this.postProcessor = Objects.requireNonNullElseGet(postProcessor, () -> + ScannerPostProcessor.builder().build()); } /** * Generate a WFP/Fingerprint for the given file @@ -400,7 +408,8 @@ public String scanFile(@NonNull String filename) throws ScannerException, Winnow * @return List of scan result strings (in JSON format) */ public List scanFolder(@NonNull String folder) { - return processFolder(folder, scanFileProcessor); + List results = processFolder(folder, scanFileProcessor); + return postProcessResults(results); } /** @@ -411,7 +420,22 @@ public List scanFolder(@NonNull String folder) { * @return List of scan result strings (in JSON format) */ public List scanFileList(@NonNull String folder, @NonNull List files) { - return processFileList(folder, files, scanFileProcessor); + List results = processFileList(folder, files, scanFileProcessor); + return postProcessResults(results); + } + + /** + * Post-processes scan results based on BOM (Bill of Materials) settings if available. + * @param results List of raw scan results in JSON string format + * @return Processed results, either modified based on BOM or original results if no BOM exists + */ + private List postProcessResults(List results) { + if (settings.getBom() != null) { + List scanFileResults = JsonUtils.toScanFileResults(results); + List newScanFileResults = this.postProcessor.process(scanFileResults, this.settings.getBom()); + return JsonUtils.toRawJsonString(newScanFileResults); + } + return results; } } \ No newline at end of file diff --git a/src/main/java/com/scanoss/ScannerPostProcessor.java b/src/main/java/com/scanoss/ScannerPostProcessor.java new file mode 100644 index 0000000..009fa3a --- /dev/null +++ b/src/main/java/com/scanoss/ScannerPostProcessor.java @@ -0,0 +1,467 @@ +// 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.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.scanoss.dto.*; +import com.scanoss.dto.enums.MatchType; +import com.scanoss.settings.Bom; +import com.scanoss.settings.RemoveRule; +import com.scanoss.settings.ReplaceRule; +import com.scanoss.settings.Rule; +import com.scanoss.utils.LineRange; +import com.scanoss.utils.LineRangeUtils; +import com.scanoss.utils.Purl2Url; +import lombok.Builder; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Post-processor for SCANOSS scanner results that applies BOM (Bill of Materials) rules + * to modify scan results after the scanning process. This processor handles two main + * operations: + *

+ * 1. Remove rules + * 2. Replace rules + *

+ * + * The processor maintains an internal Purl2ComponentMap for efficient lookup and + * transformation of components during the post-processing phase. + * @see Bom + * @see ScanFileResult + * @see ReplaceRule + */ +@Slf4j +@Builder +public class ScannerPostProcessor { + /** + * Maps purl to Component (ScanFileDetail) + */ + private Map purl2ComponentDetailsMap; + + /** + * Processes scan results according to BOM configuration rules. + * Applies remove, and replace rules as specified in the configuration. + * + * @param scanFileResults List of scan results to process + * @param bom Bom containing BOM rules + * @return List of processed scan results + */ + public List process(@NonNull List scanFileResults, @NonNull Bom bom) { + int removeSize = bom.getRemoveSize(); + int replaceSize = bom.getReplaceSize(); + log.info("Starting scan results processing with {} results", scanFileResults.size()); + log.debug("BOM configuration - Remove rules: {}, Replace rules: {}", removeSize, replaceSize); + + buildPurl2ComponentDetailsMap(scanFileResults); + List processedResults = new ArrayList<>(scanFileResults); + if (removeSize > 0) { + log.info("Applying {} remove rules to scan results", removeSize); + applyRemoveRules(processedResults, bom.getRemove()); + } + if (replaceSize > 0) { + log.info("Applying {} replace rules to scan results", replaceSize); + applyReplaceRules(processedResults, bom.getReplaceRulesByPriority()); + } + log.info("Scan results processing completed. Original results: {}, Processed results: {}", + scanFileResults.size(), processedResults.size()); + return processedResults; + } + + /** + * Creates a lookup map that links PURLs to their corresponding component details. + * This map enables efficient component lookup during the replacement process. + * + * @param scanFileResults List of scan results to process + */ + private void buildPurl2ComponentDetailsMap(@NonNull List scanFileResults) { + log.debug("Creating Purl Component Map from scan results"); + purl2ComponentDetailsMap = new HashMap<>(); + for (ScanFileResult result : scanFileResults) { + List fileDetails = result != null ? result.getFileDetails() : null; + if (fileDetails == null) { + log.warn("Null result or empty scan file result. Skipping: {}", result); + continue; + } + // Iterate through file details + for (ScanFileDetails details : fileDetails) { + String[] purls = details != null ? details.getPurls() : null; + if (purls == null) { + log.warn("Null details or empty scan file result details. Skipping: {}", details); + continue; + } + // Iterate through purls for each detail + for (String purl : purls) { + String trimmedPurl = purl != null ? purl.trim() : ""; + if (trimmedPurl.isEmpty()) { + log.warn("Empty purl details found. Skipping: {}", details); + continue; + } + // Only store if purl not already in map + if (!purl2ComponentDetailsMap.containsKey(trimmedPurl)) { + purl2ComponentDetailsMap.put(trimmedPurl, details); + } + } + } + } + log.debug("Purl Component Map created with {} entries", purl2ComponentDetailsMap.size()); + } + + + /** + * Applies replacement rules to scan results, updating their PURLs (Package URLs) based on matching rules. + * If a cached component exists for a replacement PURL, it will be used instead of creating a new one. + * + * @param results The list of scan results to process and modify + * @param rules The list of replacement rules to apply + * @return The modified input list of scan results with updated PURLs + */ + private void applyReplaceRules(@NonNull List results, @NonNull List rules) { + log.debug("Starting replace rules application for {} results with {} rules", results.size(), rules.size()); + results.forEach(result -> applyReplaceRulesOnResult(result, rules)); + } + + + /** + * Processes a single scan result against all replacement rules. + * Applies the first matching rule found to update the result's package information. + * + * @param result The scan result to process + * @param rules List of replacement rules to check against + */ + private void applyReplaceRulesOnResult(@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; + } + + if (hasNoValidMatch(result)) { + log.debug("Scan result has no valid matches for file: {}", result.getFilePath()); + return; + } + // Find the first matching rule and apply its replacement + // This ensures only one rule is applied per result, maintaining consistency + rules.stream() + .filter(rule -> isMatchingRule(result, rule)) + .findFirst() + .ifPresent(rule -> updateResultWithReplaceRule(result, rule)); + } + + /** + * Updates a scan result using the specified replacement rule. + * Creates a new package URL from the rule and updates all component details + * within the scan result to use the new package information. + * + * @param result The scan result to update + * @param rule The replacement rule containing the new package URL + */ + private void updateResultWithReplaceRule(@NonNull ScanFileResult result, @NonNull ReplaceRule rule) { + PackageURL newPurl = createPackageUrl(rule); + if (newPurl == null) + return; + List 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:

+ *
    + *
  1. Priority score based on the presence of PURL and path attributes
  2. + *
  3. Path length comparison (if priority scores are equal)
  4. + *
+ * + *

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:

+ *
    + *
  1. Calculate priority scores for both rules
  2. + *
  3. If scores differ, higher score takes precedence
  4. + *
  5. If scores are equal and both rules have paths, longer path takes precedence
  6. + *
  7. If no differentiation is possible, rules are considered equal
  8. + *
+ * + * @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" + } + ] + } +} +