diff --git a/build.gradle b/build.gradle index 17a45f08..92fdc514 100644 --- a/build.gradle +++ b/build.gradle @@ -72,10 +72,9 @@ repositories { } } -def buildInfoVersion = '2.43.6' -def idePluginsCommonVersion = '2.4.4' -// Updated to 2.17.3 for security fixes - compatible with Java 8+ -def jacksonVersion = '2.17.3' +def buildInfoVersion = '2.43.9' +def idePluginsCommonVersion = '2.4.5' +def jacksonVersion = '2.18.6' dependencies { implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: jacksonVersion @@ -87,7 +86,7 @@ dependencies { implementation group: 'com.jfrog.xray.client', name: 'xray-client-java', version: '0.14.1' implementation group: 'org.apache.commons', name: 'commons-collections4', version: '4.4' implementation group: 'org.jfrog.filespecs', name: 'file-specs-java', version: '1.1.2' - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.11' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.18.0' implementation group: 'com.google.guava', name: 'guava', version: '32.0.1-jre' implementation group: 'org.codehaus.plexus', name: 'plexus-utils', version: '3.4.1' implementation group: 'net.lingala.zip4j', name: 'zip4j', version: '2.11.4' diff --git a/src/main/java/com/jfrog/ide/idea/ci/CiManager.java b/src/main/java/com/jfrog/ide/idea/ci/CiManager.java index b284c2e7..18e9657e 100644 --- a/src/main/java/com/jfrog/ide/idea/ci/CiManager.java +++ b/src/main/java/com/jfrog/ide/idea/ci/CiManager.java @@ -24,6 +24,7 @@ import com.jfrog.ide.idea.ui.menus.filtermanager.CiFilterManager; import com.jfrog.ide.idea.utils.Utils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.NotNull; import org.jfrog.build.extractor.scan.DependencyTree; @@ -151,8 +152,8 @@ public BuildGeneralInfo getBuildGeneralInfo(String buildIdentifier) { return (BuildGeneralInfo) root.getChildren().stream() .map(DependencyTree::getGeneralInfo) .map(generalInfo -> (BuildGeneralInfo) generalInfo) - .filter(generalInfo -> StringUtils.equals(buildName, generalInfo.getBuildName())) - .filter(generalInfo -> StringUtils.equals(buildNumber, generalInfo.getBuildNumber())) + .filter(generalInfo -> Strings.CS.equals(buildName, generalInfo.getBuildName())) + .filter(generalInfo -> Strings.CS.equals(buildNumber, generalInfo.getBuildNumber())) .findAny().orElse(null); } diff --git a/src/main/java/com/jfrog/ide/idea/configuration/ServerConfigImpl.java b/src/main/java/com/jfrog/ide/idea/configuration/ServerConfigImpl.java index d07cb734..80ca2a43 100644 --- a/src/main/java/com/jfrog/ide/idea/configuration/ServerConfigImpl.java +++ b/src/main/java/com/jfrog/ide/idea/configuration/ServerConfigImpl.java @@ -33,6 +33,7 @@ import com.jfrog.ide.idea.ui.configuration.ConnectionRetriesSpinner; import com.jfrog.ide.idea.ui.configuration.ConnectionTimeoutSpinner; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.Strings; import org.jfrog.build.client.ProxyConfiguration; import javax.annotation.CheckForNull; @@ -51,7 +52,9 @@ import static com.jfrog.ide.idea.ui.configuration.ConfigVerificationUtils.DEFAULT_EXCLUSIONS; import static com.jfrog.ide.idea.ui.configuration.Utils.*; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.ObjectUtils.getIfNull; import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.Strings.CS; /** * @author yahavi @@ -234,7 +237,7 @@ public boolean isInsecureTls() { } public String getExcludedPaths() { - return defaultIfNull(this.excludedPaths, DEFAULT_EXCLUSIONS); + return getIfNull(this.excludedPaths, DEFAULT_EXCLUSIONS); } @Override @@ -259,12 +262,12 @@ public SSLContext getSslContext() { @Override public int getConnectionRetries() { - return defaultIfNull(this.connectionRetries, ConnectionRetriesSpinner.RANGE.initial); + return getIfNull(this.connectionRetries, ConnectionRetriesSpinner.RANGE.initial); } @Override public int getConnectionTimeout() { - return defaultIfNull(this.connectionTimeout, ConnectionTimeoutSpinner.RANGE.initial); + return getIfNull(this.connectionTimeout, ConnectionTimeoutSpinner.RANGE.initial); } @Override @@ -456,7 +459,7 @@ public void readConnectionDetailsFromEnv() { } setUrl(platformUrlEnv); - String platformUrlStr = removeEnd(platformUrlEnv, "/"); + String platformUrlStr = Strings.CS.removeEnd(platformUrlEnv, "/"); if (isBlank(xrayUrlEnv)) { setXrayUrl(platformUrlStr + "/xray"); } else { diff --git a/src/main/java/com/jfrog/ide/idea/inspections/AbstractInspection.java b/src/main/java/com/jfrog/ide/idea/inspections/AbstractInspection.java index 40a21fc3..c66949a5 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/AbstractInspection.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/AbstractInspection.java @@ -21,12 +21,14 @@ import com.jfrog.ide.idea.inspections.upgradeversion.UpgradeVersion; import com.jfrog.ide.idea.navigation.NavigationService; import com.jfrog.ide.idea.scan.ScannerBase; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.LocalComponentsTree; import com.jfrog.ide.idea.utils.Descriptor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import javax.swing.tree.TreeNode; import java.util.*; @@ -142,7 +144,7 @@ Set getFileDescriptors(PsiElement element) { Enumeration roots = ((SortableChildrenTreeNode) componentsTree.getModel().getRoot()).children(); for (TreeNode root : Collections.list(roots)) { if (root instanceof DescriptorFileTreeNode fileNode) { - if (fileNode.getFilePath().equals(element.getContainingFile().getVirtualFile().getPath())) { + if (DescriptorPathUtils.areDescriptorPathsEqual(fileNode.getFilePath(), element.getContainingFile().getVirtualFile().getPath())) { fileDescriptors.add(fileNode); } } @@ -236,13 +238,13 @@ boolean isNodeMatch(DependencyNode node, String componentName) { String artifactID = node.getComponentIdWithoutPrefix(); ImpactTree impactTree = node.getImpactTree(); String versionPrefix = ":"; - return StringUtils.equals(extractArtifactIdWithoutVersion(artifactID), componentName) || impactTree.contains(componentName+versionPrefix); + return Strings.CS.equals(extractArtifactIdWithoutVersion(artifactID), componentName) || impactTree.contains(componentName+versionPrefix); } abstract UpgradeVersion getUpgradeVersion(String componentName, String fixVersion, Collection issues, String descriptorPath); void registerProblem(ProblemsHolder problemsHolder, DependencyNode dependency, PsiElement element, String componentName) { - boolean isTransitive = dependency.isIndirect() || !StringUtils.contains(dependency.getTitle(), componentName); + boolean isTransitive = dependency.isIndirect() || !Strings.CS.contains(dependency.getTitle(), componentName); String dependencyDescription = getDependencyDescription(dependency.getTitle(), isTransitive); List quickFixes = new ArrayList<>(); quickFixes.add(new ShowInDependencyTree(dependency, dependencyDescription)); diff --git a/src/main/java/com/jfrog/ide/idea/inspections/GoInspection.java b/src/main/java/com/jfrog/ide/idea/inspections/GoInspection.java index ec32e9b5..1e182c27 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/GoInspection.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/GoInspection.java @@ -10,9 +10,11 @@ import com.intellij.psi.PsiElementVisitor; import com.jfrog.ide.idea.inspections.upgradeversion.GoUpgradeVersion; import com.jfrog.ide.idea.inspections.upgradeversion.UpgradeVersion; +import com.jfrog.ide.idea.scan.GoScanner; import com.jfrog.ide.idea.scan.ScanManager; import com.jfrog.ide.idea.scan.ScannerBase; import com.jfrog.ide.idea.utils.Descriptor; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -56,7 +58,8 @@ boolean isDependency(PsiElement element) { @Override ScannerBase getScanner(Project project, String path) { return ScanManager.getScanners(project).stream() - .filter(manager -> StringUtils.equals(manager.getProjectPath(), path)) + .filter(GoScanner.class::isInstance) + .filter(manager -> DescriptorPathUtils.areDescriptorPathsEqual(manager.getProjectPath(), path)) .findAny() .orElse(null); } diff --git a/src/main/java/com/jfrog/ide/idea/inspections/GradleGroovyInspection.java b/src/main/java/com/jfrog/ide/idea/inspections/GradleGroovyInspection.java index f09c6186..a1aea36a 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/GradleGroovyInspection.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/GradleGroovyInspection.java @@ -8,6 +8,7 @@ import com.jfrog.ide.idea.inspections.upgradeversion.UpgradeVersion; import com.jfrog.ide.idea.utils.Descriptor; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.groovy.lang.psi.GroovyElementVisitor; import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement; @@ -23,6 +24,8 @@ import java.util.List; import java.util.Objects; +import static org.apache.commons.lang3.Strings.CS; + /** * @author yahavi */ @@ -74,7 +77,7 @@ public void visitArgumentList(@NotNull GrArgumentList list) { boolean isDependency(PsiElement element) { PsiElement parent = element.getParent(); for (int i = 0; i < 6; i++, parent = parent.getParent()) { - if (StringUtils.startsWith(parent.getText(), "dependencies")) { + if (Strings.CS.startsWith(parent.getText(), "dependencies")) { return true; } } diff --git a/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityAnnotator.java b/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityAnnotator.java index f4c120a9..943eafa3 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityAnnotator.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityAnnotator.java @@ -15,6 +15,7 @@ import com.jfrog.ide.idea.events.AnnotationEvents; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.LocalComponentsTree; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -52,7 +53,7 @@ public List doAnnotate(PsiFile file) { Enumeration roots = ((SortableChildrenTreeNode) componentsTree.getModel().getRoot()).children(); roots.asIterator().forEachRemaining(root -> { FileTreeNode fileNode = (FileTreeNode) root; - if (fileNode.getFilePath().equals(file.getContainingFile().getVirtualFile().getPath())) { + if (DescriptorPathUtils.areDescriptorPathsEqual(fileNode.getFilePath(), file.getContainingFile().getVirtualFile().getPath())) { fileNode.children().asIterator().forEachRemaining(issueNode -> { if (issueNode instanceof FileIssueNode) { issues.add((FileIssueNode) issueNode); diff --git a/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityWarning.java b/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityWarning.java index 8ade0bc2..ac44e648 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityWarning.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/JFrogSecurityWarning.java @@ -4,10 +4,10 @@ import com.jfrog.ide.common.nodes.subentities.Severity; import com.jfrog.ide.common.nodes.subentities.SourceCodeScanType; import com.jfrog.ide.idea.scan.data.*; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import lombok.Getter; +import org.jetbrains.annotations.Nullable; -import java.net.URI; -import java.nio.file.Paths; import java.util.List; @Getter @@ -53,18 +53,22 @@ public JFrogSecurityWarning( } public JFrogSecurityWarning(SarifResult result, SourceCodeScanType reporter, Rule rule) { + this(result, reporter, rule, null); + } + + public JFrogSecurityWarning(SarifResult result, SourceCodeScanType reporter, Rule rule, @Nullable String wslDistro) { this(getFirstRegion(result).getStartLine() - 1, getFirstRegion(result).getStartColumn() - 1, getFirstRegion(result).getEndLine() - 1, getFirstRegion(result).getEndColumn() - 1, determineReason(result.getMessage().getText(), rule.getShortDescription().getText(), reporter), - getFilePath(result), + getFilePath(result, wslDistro), result.getRuleId(), getFirstRegion(result).getSnippet().getText(), reporter, isWarningApplicable(result, rule), Severity.fromSarif(result.getSeverity()), - convertCodeFlowsToFindingInfo(result.getCodeFlows()) + convertCodeFlowsToFindingInfo(result.getCodeFlows(), wslDistro) ); } @@ -72,11 +76,14 @@ private static boolean isWarningApplicable(SarifResult result, Rule rule) { return !result.getKind().equals("pass") && (rule.getRuleProperties().map(properties -> properties.getApplicability().equals("applicable")).orElse(true)); } - private static String getFilePath(SarifResult result) { - return !result.getLocations().isEmpty() ? uriToPath(result.getLocations().get(0).getPhysicalLocation().getArtifactLocation().getUri()) : ""; + private static String getFilePath(SarifResult result, @Nullable String wslDistro) { + return !result.getLocations().isEmpty() + ? DescriptorPathUtils.sarifArtifactUriToLocalPath( + result.getLocations().get(0).getPhysicalLocation().getArtifactLocation().getUri(), wslDistro) + : ""; } - private static FindingInfo[][] convertCodeFlowsToFindingInfo(List codeFlows) { + private static FindingInfo[][] convertCodeFlowsToFindingInfo(List codeFlows, @Nullable String wslDistro) { if (codeFlows == null || codeFlows.isEmpty()) { return null; } @@ -92,7 +99,7 @@ private static FindingInfo[][] convertCodeFlowsToFindingInfo(List code for (int j = 0; j < locations.size(); j++) { PhysicalLocation location = locations.get(j).getLocation().getPhysicalLocation(); results[i][j] = new FindingInfo( - uriToPath(location.getArtifactLocation().getUri()), + DescriptorPathUtils.sarifArtifactUriToLocalPath(location.getArtifactLocation().getUri(), wslDistro), location.getRegion().getStartLine(), location.getRegion().getStartColumn(), location.getRegion().getEndLine(), @@ -122,10 +129,6 @@ public void setScannerSearchTarget(String scannerSearchTarget) { this.scannerSearchTarget = scannerSearchTarget; } - private static String uriToPath(String path) { - return Paths.get(URI.create(path)).toString(); - } - private static String determineReason(String resultMessage, String ruleMessage, SourceCodeScanType scannerType) { return scannerType.equals(SourceCodeScanType.SAST) ? ruleMessage : resultMessage; } diff --git a/src/main/java/com/jfrog/ide/idea/inspections/JumpToCode.java b/src/main/java/com/jfrog/ide/idea/inspections/JumpToCode.java index ea554a41..064cc635 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/JumpToCode.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/JumpToCode.java @@ -9,10 +9,10 @@ import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiUtilBase; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import org.jetbrains.annotations.NotNull; /** @@ -72,7 +72,10 @@ private void highlightCode(int startRow, int endRow, int startColumn, int endCol } private VirtualFile getVirtualFile(String path) { - return LocalFileSystem.getInstance().findFileByPath(path); + if (path == null || path.isEmpty()) { + return null; + } + return DescriptorPathUtils.findLocalVirtualFile(path); } private Document getDocument(Editor editor) { diff --git a/src/main/java/com/jfrog/ide/idea/inspections/MavenInspection.java b/src/main/java/com/jfrog/ide/idea/inspections/MavenInspection.java index eef7c463..461446a4 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/MavenInspection.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/MavenInspection.java @@ -17,6 +17,7 @@ import com.jfrog.ide.idea.scan.ScannerBase; import com.jfrog.ide.idea.utils.Descriptor; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.NotNull; import org.jetbrains.idea.maven.dom.model.MavenDomArtifactCoordinates; import java.util.Collection; @@ -58,19 +59,19 @@ public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder hold } boolean isDependencyOrPlugin(XmlTag xmlTag) { - return StringUtils.equalsAny(xmlTag.getName(), MAVEN_DEPENDENCY_TAG, MAVEN_PLUGIN_TAG); + return Strings.CS.equalsAny(xmlTag.getName(), MAVEN_DEPENDENCY_TAG, MAVEN_PLUGIN_TAG); } @Override boolean isDependency(PsiElement element) { PsiElement parentElement = element.getParent(); if ((parentElement instanceof XmlTag) && - StringUtils.equalsAny(((XmlTag) parentElement).getName(), MAVEN_DEPENDENCIES_TAG, MAVEN_PLUGINS_TAG)) { + Strings.CS.equalsAny(((XmlTag) parentElement).getName(), MAVEN_DEPENDENCIES_TAG, MAVEN_PLUGINS_TAG)) { return true; } PsiElement grandParentElement = parentElement.getParent(); return (grandParentElement instanceof XmlTag && - StringUtils.equals(((XmlTag) grandParentElement).getName(), MAVEN_DEPENDENCY_MANAGEMENT)); + Strings.CS.equals(((XmlTag) grandParentElement).getName(), MAVEN_DEPENDENCY_MANAGEMENT)); } @Override diff --git a/src/main/java/com/jfrog/ide/idea/inspections/NpmInspection.java b/src/main/java/com/jfrog/ide/idea/inspections/NpmInspection.java index 0729287f..a6a2d37a 100644 --- a/src/main/java/com/jfrog/ide/idea/inspections/NpmInspection.java +++ b/src/main/java/com/jfrog/ide/idea/inspections/NpmInspection.java @@ -13,7 +13,9 @@ import com.jfrog.ide.idea.scan.ScanManager; import com.jfrog.ide.idea.scan.ScannerBase; import com.jfrog.ide.idea.utils.Descriptor; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -49,13 +51,13 @@ public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder hold @Override boolean isDependency(PsiElement element) { PsiElement parentElement = element.getParent().getParent(); - return parentElement != null && StringUtils.equalsAny(parentElement.getFirstChild().getText(), "\"dependencies\"", "\"devDependencies\""); + return parentElement != null && Strings.CS.equalsAny(parentElement.getFirstChild().getText(), "\"dependencies\"", "\"devDependencies\""); } @Override ScannerBase getScanner(Project project, String path) { return ScanManager.getScanners(project).stream() - .filter(manager -> StringUtils.equals(manager.getProjectPath(), path)) + .filter(manager -> DescriptorPathUtils.areDescriptorPathsEqual(manager.getProjectPath(), path)) .filter(this::isMatchingScanner) .findAny() .orElse(null); diff --git a/src/main/java/com/jfrog/ide/idea/scan/ApplicabilityScannerExecutor.java b/src/main/java/com/jfrog/ide/idea/scan/ApplicabilityScannerExecutor.java index 8c5283e4..ec773d11 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/ApplicabilityScannerExecutor.java +++ b/src/main/java/com/jfrog/ide/idea/scan/ApplicabilityScannerExecutor.java @@ -9,6 +9,7 @@ import com.jfrog.ide.idea.scan.data.*; import com.jfrog.xray.client.services.entitlements.Feature; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jfrog.build.api.util.Log; import java.io.IOException; @@ -28,6 +29,11 @@ public ApplicabilityScannerExecutor(Log log) { supportedPackageTypes = SUPPORTED_PACKAGE_TYPES; } + public ApplicabilityScannerExecutor(Log log, String wslDistro) { + super(SourceCodeScanType.CONTEXTUAL, log, wslDistro); + supportedPackageTypes = SUPPORTED_PACKAGE_TYPES; + } + public List execute(ScanConfig.Builder inputFileBuilder, Runnable checkCanceled, ProgressIndicator indicator) throws IOException, InterruptedException { return super.execute(inputFileBuilder, SCANNER_ARGS, checkCanceled, indicator); } @@ -57,7 +63,7 @@ protected List parseOutputSarif(Path outputFile) throws IO List evidence = resultsByRule.getOrDefault(rule.getId(), List.of()); for (SarifResult result : evidence) { if (!result.getLocations().isEmpty()) { - warnings.add(new JFrogSecurityWarning(result, scanType, rule)); + warnings.add(new JFrogSecurityWarning(result, scanType, rule, getWslDistro())); } } } else if ("not_applicable".equals(applicability)) { @@ -96,7 +102,7 @@ List createSpecificFileIssueNodes(List warni HashMap results = new HashMap<>(); for (JFrogSecurityWarning warning : warnings) { // Update all VulnerabilityNodes that have the warning's CVE - String cve = StringUtils.removeStart(warning.getRuleID(), "applic_"); + String cve = Strings.CS.removeStart(warning.getRuleID(), "applic_"); List issues = issuesMap.get(cve); if (issues != null) { if (warning.isApplicable()) { diff --git a/src/main/java/com/jfrog/ide/idea/scan/GoScanner.java b/src/main/java/com/jfrog/ide/idea/scan/GoScanner.java index c3820748..4f0e78e3 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/GoScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/GoScanner.java @@ -3,7 +3,6 @@ import com.google.common.collect.Maps; import com.intellij.diagnostic.PluginException; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; @@ -17,6 +16,7 @@ import com.jfrog.ide.idea.scan.data.PackageManagerType; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import com.jfrog.ide.idea.utils.GoUtils; import javax.annotation.Nullable; @@ -58,7 +58,7 @@ protected DepTree buildTree() throws IOException { @Override protected PsiFile[] getProjectDescriptors() { - VirtualFile file = LocalFileSystem.getInstance().findFileByPath(descriptorFilePath); + VirtualFile file = DescriptorPathUtils.findLocalVirtualFile(descriptorFilePath); if (file == null) { return null; } diff --git a/src/main/java/com/jfrog/ide/idea/scan/GradleScanner.java b/src/main/java/com/jfrog/ide/idea/scan/GradleScanner.java index e25b77ee..f54ec5d8 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/GradleScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/GradleScanner.java @@ -5,7 +5,6 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; @@ -19,6 +18,7 @@ import com.jfrog.ide.idea.inspections.GradleKotlinInspection; import com.jfrog.ide.idea.log.Logger; import com.jfrog.ide.idea.scan.data.PackageManagerType; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; import org.apache.commons.lang3.StringUtils; @@ -71,11 +71,10 @@ public class GradleScanner extends SingleDescriptorScanner { @Override protected PsiFile[] getProjectDescriptors() { - LocalFileSystem localFileSystem = LocalFileSystem.getInstance(); Path basePath = Paths.get(this.basePath); - VirtualFile file = localFileSystem.findFileByPath(basePath.resolve("build.gradle").toString()); + VirtualFile file = DescriptorPathUtils.findLocalVirtualFile(basePath.resolve("build.gradle").toString()); if (file == null) { - file = localFileSystem.findFileByPath(basePath.resolve("build.gradle.kts").toString()); + file = DescriptorPathUtils.findLocalVirtualFile(basePath.resolve("build.gradle.kts").toString()); if (file == null) { return null; } diff --git a/src/main/java/com/jfrog/ide/idea/scan/IACScannerExecutor.java b/src/main/java/com/jfrog/ide/idea/scan/IACScannerExecutor.java index ce31ca2c..dc835d7f 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/IACScannerExecutor.java +++ b/src/main/java/com/jfrog/ide/idea/scan/IACScannerExecutor.java @@ -26,6 +26,10 @@ public IACScannerExecutor(Log log) { super(SourceCodeScanType.IAC, log); } + public IACScannerExecutor(Log log, String wslDistro) { + super(SourceCodeScanType.IAC, log, wslDistro); + } + public List execute(ScanConfig.Builder inputFileBuilder, Runnable checkCanceled, ProgressIndicator indicator) throws IOException, InterruptedException { return super.execute(inputFileBuilder, SCANNER_ARGS, checkCanceled, indicator); } diff --git a/src/main/java/com/jfrog/ide/idea/scan/NpmScanner.java b/src/main/java/com/jfrog/ide/idea/scan/NpmScanner.java index fd5d6887..1af8434a 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/NpmScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/NpmScanner.java @@ -1,7 +1,6 @@ package com.jfrog.ide.idea.scan; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; @@ -15,6 +14,7 @@ import com.jfrog.ide.idea.scan.data.PackageManagerType; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import java.io.IOException; import java.nio.file.Paths; @@ -46,7 +46,7 @@ protected DepTree buildTree() throws IOException { @Override protected PsiFile[] getProjectDescriptors() { - VirtualFile file = LocalFileSystem.getInstance().findFileByPath(descriptorFilePath); + VirtualFile file = DescriptorPathUtils.findLocalVirtualFile(descriptorFilePath); if (file == null) { return null; } diff --git a/src/main/java/com/jfrog/ide/idea/scan/SastScannerExecutor.java b/src/main/java/com/jfrog/ide/idea/scan/SastScannerExecutor.java index f807e6bc..841cb419 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/SastScannerExecutor.java +++ b/src/main/java/com/jfrog/ide/idea/scan/SastScannerExecutor.java @@ -31,6 +31,10 @@ public SastScannerExecutor(Log log) { super(SourceCodeScanType.SAST, log); } + public SastScannerExecutor(Log log, String wslDistro) { + super(SourceCodeScanType.SAST, log, wslDistro); + } + public List execute(ScanConfig.Builder inputFileBuilder, Runnable checkCanceled, ProgressIndicator indicator) throws IOException, InterruptedException { return super.execute(inputFileBuilder, SCANNER_ARGS, checkCanceled, RUN_WITH_NEW_CONFIG_FILE, indicator); } diff --git a/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java b/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java index 60594a08..8dcaaea2 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java +++ b/src/main/java/com/jfrog/ide/idea/scan/ScanBinaryExecutor.java @@ -20,6 +20,7 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.SystemUtils; import org.apache.http.Header; import org.jfrog.build.api.util.Log; @@ -29,6 +30,8 @@ import org.jfrog.build.extractor.clientConfiguration.client.artifactory.ArtifactoryManager; import org.jfrog.build.extractor.executor.CommandExecutor; import org.jfrog.build.extractor.executor.CommandResults; +import org.jfrog.build.extractor.WslUtils; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileInputStream; @@ -39,13 +42,17 @@ import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.jfrog.ide.common.utils.ArtifactoryConnectionUtils.createAnonymousAccessArtifactoryManagerBuilder; import static com.jfrog.ide.common.utils.ArtifactoryConnectionUtils.createArtifactoryManagerBuilder; import static com.jfrog.ide.common.utils.Utils.createMapper; import static com.jfrog.ide.common.utils.Utils.createYAMLMapper; import static com.jfrog.ide.common.utils.XrayConnectionUtils.createXrayClientBuilder; +import com.jfrog.ide.idea.scan.utils.ScanUtils; + import static com.jfrog.ide.idea.scan.utils.ScanUtils.getOSAndArc; +import static com.jfrog.ide.idea.utils.DescriptorPathUtils.WIN_WSL_PREFIX; import static com.jfrog.ide.idea.utils.Utils.HOME_PATH; import static java.lang.String.join; @@ -58,7 +65,7 @@ public abstract class ScanBinaryExecutor { private static final int USER_NOT_ENTITLED = 31; private static final int NOT_SUPPORTED = 13; private static final String SCANNER_BINARY_NAME = "analyzerManager"; - static final String DEFAULT_SCANNER_BINARY_VERSION = "1.30.1"; + static final String DEFAULT_SCANNER_BINARY_VERSION = "1.31.1"; private static final String BINARY_DOWNLOAD_URL_PREFIX = "xsc-gen-exe-analyzer-manager-local/v1/"; private static final String DOWNLOAD_SCANNER_NAME = "analyzerManager.zip"; private static final String MINIMAL_XRAY_VERSION_SUPPORTED_FOR_ENTITLEMENT = "3.66.0"; @@ -68,10 +75,10 @@ public abstract class ScanBinaryExecutor { private static final String ENV_ACCESS_TOKEN = "JF_TOKEN"; private static final String ENV_HTTP_PROXY = "HTTP_PROXY"; private static final String JFROG_RELEASES = "https://releases.jfrog.io/artifactory/"; - private static Path binaryTargetPath; - private static Path archiveTargetPath; + private Path binaryTargetPath; + private Path archiveTargetPath; @Getter - private static String osDistribution; + private String osDistribution; private static LocalDateTime nextUpdateCheck; private static String lastDownloadedVersion; protected final SourceCodeScanType scanType; @@ -79,14 +86,52 @@ public abstract class ScanBinaryExecutor { private final Log log; private boolean notSupported; private final static Object downloadLock = new Object(); + private final @Nullable String wslDistro; + private final @Nullable String binaryLinuxPath; + + /** + * Returns the WSL distro name when scanning a WSL-hosted project + */ + protected @Nullable String getWslDistro() { + return wslDistro; + } ScanBinaryExecutor(SourceCodeScanType scanType, Log log) { this.scanType = scanType; this.log = log; + this.wslDistro = null; + this.binaryLinuxPath = null; String executable = SystemUtils.IS_OS_WINDOWS ? SCANNER_BINARY_NAME + ".exe" : SCANNER_BINARY_NAME; binaryTargetPath = BINARIES_DIR.resolve(SCANNER_BINARY_NAME).resolve(executable); archiveTargetPath = BINARIES_DIR.resolve(DOWNLOAD_SCANNER_NAME); - setOsDistribution(); + setOsDistribution(log); + } + + ScanBinaryExecutor(SourceCodeScanType scanType, Log log, String distro) { + this.scanType = scanType; + this.log = log; + this.wslDistro = distro; + String linuxPath = null; + Path targetPath = null; + Path archivePath = null; + try { + String linuxHome = ScanUtils.getWslLinuxHome(distro); + String linuxBinDir = linuxHome + "/.jfrog-idea-plugin/dependencies/jfrog-security"; + linuxPath = linuxBinDir + "/" + SCANNER_BINARY_NAME + "/" + SCANNER_BINARY_NAME; + targetPath = toWslUncPath(distro, linuxPath); + archivePath = toWslUncPath(distro, linuxBinDir + "/" + DOWNLOAD_SCANNER_NAME); + } catch (IOException e) { + log.warn("Could not determine WSL home directory for distro '" + distro + "': " + e.getMessage()); + notSupported = true; + } + this.binaryLinuxPath = linuxPath; + this.binaryTargetPath = targetPath; + this.archiveTargetPath = archivePath; + setOsDistribution(log); + } + + private static Path toWslUncPath(String distro, String linuxPath) { + return Path.of(WIN_WSL_PREFIX + distro + linuxPath.replace('/', '\\')); } private ArtifactoryManagerBuilder createManagerBuilder(boolean useJFrogReleases, ServerConfig server) { @@ -102,8 +147,12 @@ private ArtifactoryManagerBuilder createManagerBuilder(boolean useJFrogReleases, return null; } - protected void setOsDistribution() { + protected void setOsDistribution(Log log) { try { + if (wslDistro != null) { + osDistribution = ScanUtils.getWslArch(wslDistro, log); + return; + } osDistribution = getOSAndArc(); } catch (IOException e) { log.warn(e.getMessage()); @@ -140,39 +189,100 @@ protected List execute(ScanConfig.Builder inputFileBuilder return execute(inputFileBuilder, args, checkCanceled, false, indicator); } + /** + * WSL vs native is decided from per-executor instance state (not statics). An early WSL variant used + * static flags cleared by {@link #ScanBinaryExecutor(SourceCodeScanType, Log)}; that could mix + * "native" control flow with WSL UNC binary paths when multiple projects/managers shared the JVM. + */ + private boolean useWslRuntime() { + return wslDistro != null && binaryLinuxPath != null && !binaryLinuxPath.isEmpty(); + } + + private record ScanRun(Path outputTempDir, Path inputFile, Path outputFilePath, CommandResults commandResults, String cmd) { + } + protected List execute(ScanConfig.Builder inputFileBuilder, List args, Runnable checkCanceled, boolean newConfigFormat, ProgressIndicator indicator) throws IOException, InterruptedException { if (!shouldExecute()) { return List.of(); } checkCanceled.run(); updateBinaryIfNeeded(); - Path outputTempDir = null; - Path inputFile = null; - try { - outputTempDir = Files.createTempDirectory(""); - Path outputFilePath = Files.createTempFile(outputTempDir, "", ".sarif"); - inputFileBuilder.output(outputFilePath.toString()); - inputFileBuilder.scanType(scanType); - ScanConfig inputParams = inputFileBuilder.Build(); - args = new ArrayList<>(args); - inputFile = newConfigFormat ? createTempRunInputFile(new NewScansConfig(new NewScanConfig(inputParams))) : createTempRunInputFile(new ScansConfig(List.of(inputParams))); - args.add(inputFile.toString()); - if (newConfigFormat) { - args.add(outputFilePath.toString()); - } + Logger log = Logger.getInstance(); + List cmdArgs = new ArrayList<>(args); + ScanRun run = useWslRuntime() + ? runWslScan(inputFileBuilder, cmdArgs, newConfigFormat, indicator, log) + : runNativeScan(inputFileBuilder, cmdArgs, newConfigFormat, indicator, log); + checkCanceled.run(); + return handleScanCommandResult(run, log); + } + + private ScanRun runWslScan(ScanConfig.Builder inputFileBuilder, List args, boolean newConfigFormat, ProgressIndicator indicator, Logger log) throws IOException, InterruptedException { + int parentIdx = binaryLinuxPath.lastIndexOf('/'); + if (parentIdx < 0) { + throw new IOException("Invalid WSL scanner path (no directory segment): " + binaryLinuxPath); + } + String linuxParentDir = binaryLinuxPath.substring(0, parentIdx); + inputFileBuilder.scanType(scanType); + Path wslTmpBase = Path.of(WIN_WSL_PREFIX + wslDistro + "\\tmp"); + Path outputTempDir = Files.createTempDirectory(wslTmpBase, "jfrog"); + Path outputFilePath = Files.createTempFile(outputTempDir, "", ".sarif"); + String linuxOutputPath = WslUtils.toLinuxPath(outputFilePath.toString()); + + List linuxRoots = inputFileBuilder.Build().getRoots().stream() + .map(WslUtils::toLinuxPath) + .collect(Collectors.toList()); + inputFileBuilder.roots(linuxRoots).output(linuxOutputPath); + ScanConfig inputParams = inputFileBuilder.Build(); + + Path inputFile = createTempRunInputFileInWsl( + newConfigFormat ? new NewScansConfig(new NewScanConfig(inputParams)) : new ScansConfig(List.of(inputParams))); + String linuxInputPath = WslUtils.toLinuxPath(inputFile.toString()); + + Map credentialEnv = createCredentialEnv(); + Map wslEnv = createEnvWithCredentials(); + wslEnv.put("WSLENV", String.join(":", credentialEnv.keySet())); + List wslArgs = new ArrayList<>(List.of("-d", wslDistro, "--cd", linuxParentDir, "-e", binaryLinuxPath)); + wslArgs.addAll(args); + wslArgs.add(linuxInputPath); + wslArgs.add(linuxOutputPath); - Logger log = Logger.getInstance(); - // The following logging is done outside the commandExecutor because the commandExecutor log level is set to INFO. - // As it is an internal binary execution, the message should be printed for DEBUG use only. - indicator.setText(String.format("Running %s scan at %s", scanType.toString().toLowerCase(), String.join(" ", inputParams.getRoots()))); - String cmd = String.format("%s %s", binaryTargetPath.toString(), join(" ", args)); - log.info(String.format("Executing JAS scanner %s with config: %s", cmd, inputParams)); - CommandExecutor commandExecutor = new CommandExecutor(binaryTargetPath.toString(), createEnvWithCredentials()); - CommandResults commandResults = commandExecutor.exeCommand(binaryTargetPath.toFile().getParentFile(), args, - null, new NullLog(), Long.MAX_VALUE, TimeUnit.MINUTES); + indicator.setText(String.format("Running %s scan at %s", scanType.toString().toLowerCase(), String.join(" ", linuxRoots))); + String cmd = "wsl.exe " + join(" ", wslArgs); + log.info(String.format("Executing JAS scanner (WSL) %s with config: %s", cmd, inputParams)); + CommandExecutor commandExecutor = new CommandExecutor("wsl.exe", wslEnv); + CommandResults commandResults = commandExecutor.exeCommand(null, wslArgs, null, new NullLog(), Long.MAX_VALUE, TimeUnit.MINUTES); + return new ScanRun(outputTempDir, inputFile, outputFilePath, commandResults, cmd); + } + + private ScanRun runNativeScan(ScanConfig.Builder inputFileBuilder, List args, boolean newConfigFormat, ProgressIndicator indicator, Logger log) throws IOException, InterruptedException { + Path outputTempDir = Files.createTempDirectory(""); + Path outputFilePath = Files.createTempFile(outputTempDir, "", ".sarif"); + inputFileBuilder.output(outputFilePath.toString()); + inputFileBuilder.scanType(scanType); + ScanConfig inputParams = inputFileBuilder.Build(); - checkCanceled.run(); + Path inputFile = newConfigFormat ? createTempRunInputFile(new NewScansConfig(new NewScanConfig(inputParams))) : createTempRunInputFile(new ScansConfig(List.of(inputParams))); + args.add(inputFile.toString()); + if (newConfigFormat) { + args.add(outputFilePath.toString()); + } + indicator.setText(String.format("Running %s scan at %s", scanType.toString().toLowerCase(), String.join(" ", inputParams.getRoots()))); + String cmd = String.format("%s %s", binaryTargetPath.toString(), join(" ", args)); + log.info(String.format("Executing JAS scanner %s with config: %s", cmd, inputParams)); + CommandExecutor commandExecutor = new CommandExecutor(binaryTargetPath.toString(), createEnvWithCredentials()); + CommandResults commandResults = commandExecutor.exeCommand(binaryTargetPath.toFile().getParentFile(), args, + null, new NullLog(), Long.MAX_VALUE, TimeUnit.MINUTES); + return new ScanRun(outputTempDir, inputFile, outputFilePath, commandResults, cmd); + } + + private List handleScanCommandResult(ScanRun run, Logger log) throws IOException { + Path outputTempDir = run.outputTempDir(); + Path inputFile = run.inputFile(); + Path outputFilePath = run.outputFilePath(); + CommandResults commandResults = run.commandResults(); + String cmd = run.cmd(); + try { if (commandResults.isOk()) { log.info(String.format("Finished successfully to run command: %s", cmd)); log.debug(commandResults.getRes()); @@ -243,7 +353,7 @@ public String getFileChecksumFromServer(ArtifactoryManager artifactoryManager, S String url = getBinaryDownloadURL(externalResourcesRepo); Header[] headers = artifactoryManager.downloadHeaders(url); for (Header header : headers) { - if (StringUtils.equalsIgnoreCase(header.getName(), "x-checksum-sha256")) { + if (Strings.CI.equals(header.getName(), "x-checksum-sha256")) { return header.getValue(); } } @@ -280,7 +390,7 @@ protected List parseOutputSarif(Path outputFile) throws IO output.getRuns().forEach(run -> run.getResults().stream() .filter(SarifResult::isNotSuppressed) .filter(result -> !"informational".equals(result.getKind())) - .forEach(result -> warnings.add(new JFrogSecurityWarning(result, scanType, run.getRuleFromRunById(result.getRuleId()))))); + .forEach(result -> warnings.add(new JFrogSecurityWarning(result, scanType, run.getRuleFromRunById(result.getRuleId()), getWslDistro())))); Optional run = output.getRuns().stream().findFirst(); if (run.isPresent()) { @@ -316,8 +426,27 @@ protected void downloadBinary(ArtifactoryManager artifactoryManager, String exte } catch (ZipException exception) { throw new IOException("An error occurred while trying to unarchived the JFrog executable:\n" + exception.getMessage()); } - // Set executable permissions to the downloaded scanner - if (!binaryTargetPath.toFile().setExecutable(true)) { + setExecutablePermissions(); + } + + private void setExecutablePermissions() throws IOException { + if (useWslRuntime()) { + int dirSep = binaryLinuxPath.lastIndexOf('/'); + if (dirSep < 0) { + throw new IOException("Invalid WSL scanner path (no directory segment): " + binaryLinuxPath); + } + String binaryLinuxDir = binaryLinuxPath.substring(0, dirSep); + try { + Process chmod = new ProcessBuilder("wsl.exe", "-d", wslDistro, "-e", "chmod", "-R", "+x", binaryLinuxDir) + .redirectErrorStream(true).start(); + if (chmod.waitFor() != 0) { + throw new IOException("chmod -R +x failed for WSL scanner directory at " + binaryLinuxDir); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while setting WSL scanner permissions", e); + } + } else if (!binaryTargetPath.toFile().setExecutable(true)) { throw new IOException("An error occurred while trying to give access permissions to the JFrog executable."); } } @@ -330,8 +459,25 @@ Path createTempRunInputFile(Object scanInput) throws IOException { return inputPath; } - private Map createEnvWithCredentials() { - Map env = new HashMap<>(EnvironmentUtil.getEnvironmentMap()); + private Path createTempRunInputFileInWsl(Object scanInput) throws IOException { + Path wslTmpBase = Path.of(WIN_WSL_PREFIX + wslDistro + "\\tmp"); + return createTempRunInputFileInWsl(scanInput, wslTmpBase); + } + + /** + * Writes scan input YAML under a {@code jfrog*} temp subdirectory of {@code wslTmpBase}. + * Production passes the WSL distro tmp UNC root; tests pass a local writable directory. + */ + Path createTempRunInputFileInWsl(Object scanInput, Path wslTmpBase) throws IOException { + ObjectMapper om = createYAMLMapper(); + Path tempDir = Files.createTempDirectory(wslTmpBase, "jfrog"); + Path inputPath = Files.createTempFile(tempDir, "", ".yaml"); + om.writeValue(inputPath.toFile(), scanInput); + return inputPath; + } + + private Map createCredentialEnv() { + Map env = new HashMap<>(); ServerConfigImpl serverConfig = GlobalSettings.getInstance().getServerConfig(); env.put(ENV_PLATFORM, serverConfig.getUrl()); if (StringUtils.isNotEmpty(serverConfig.getAccessToken())) { @@ -340,7 +486,6 @@ private Map createEnvWithCredentials() { env.put(ENV_USER, serverConfig.getUsername()); env.put(ENV_PASSWORD, serverConfig.getPassword()); } - ProxyConfiguration proxyConfiguration = serverConfig.getProxyConfForTargetUrl(serverConfig.getUrl()); if (proxyConfiguration != null) { String proxyUrl = proxyConfiguration.host + ":" + proxyConfiguration.port; @@ -353,4 +498,10 @@ private Map createEnvWithCredentials() { return env; } + private Map createEnvWithCredentials() { + Map env = new HashMap<>(EnvironmentUtil.getEnvironmentMap()); + env.putAll(createCredentialEnv()); + return env; + } + } diff --git a/src/main/java/com/jfrog/ide/idea/scan/SecretsScannerExecutor.java b/src/main/java/com/jfrog/ide/idea/scan/SecretsScannerExecutor.java index f378283c..e2469d1f 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/SecretsScannerExecutor.java +++ b/src/main/java/com/jfrog/ide/idea/scan/SecretsScannerExecutor.java @@ -26,6 +26,10 @@ public SecretsScannerExecutor(Log log) { super(SourceCodeScanType.SECRETS, log); } + public SecretsScannerExecutor(Log log, String wslDistro) { + super(SourceCodeScanType.SECRETS, log, wslDistro); + } + public List execute(ScanConfig.Builder inputFileBuilder, Runnable checkCanceled, ProgressIndicator indicator) throws IOException, InterruptedException { return super.execute(inputFileBuilder, SCANNER_ARGS, checkCanceled, indicator); } diff --git a/src/main/java/com/jfrog/ide/idea/scan/SourceCodeScannerManager.java b/src/main/java/com/jfrog/ide/idea/scan/SourceCodeScannerManager.java index 071df92c..28af87ca 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/SourceCodeScannerManager.java +++ b/src/main/java/com/jfrog/ide/idea/scan/SourceCodeScannerManager.java @@ -19,11 +19,16 @@ import com.jfrog.ide.idea.scan.data.applications.JFrogApplicationsConfig; import com.jfrog.ide.idea.scan.data.applications.ModuleConfig; import com.jfrog.ide.idea.scan.data.applications.ScannerConfig; +import com.jfrog.ide.idea.scan.utils.ScanUtils; import com.jfrog.ide.idea.ui.LocalComponentsTree; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.NotNull; +import org.jfrog.build.extractor.WslUtils; import javax.swing.tree.TreeNode; import java.io.File; @@ -57,8 +62,8 @@ public class SourceCodeScannerManager { private final Path jfrogApplictionsConfigPath; private final AtomicBoolean scanInProgress = new AtomicBoolean(false); - private final ApplicabilityScannerExecutor applicability = new ApplicabilityScannerExecutor(Logger.getInstance()); - private final Map scanners = initScannersCollection(); + private final ApplicabilityScannerExecutor applicability; + private final Map scanners; protected Project project; protected PackageManagerType packageType; private static final String SKIP_FOLDERS_SUFFIX = "*/**"; @@ -67,6 +72,11 @@ public class SourceCodeScannerManager { public SourceCodeScannerManager(Project project) { this.project = project; this.jfrogApplictionsConfigPath = getProjectBasePath(project).resolve(".jfrog").resolve("jfrog-apps-config.yml"); + String wslDistro = detectWslDistro(project); + this.applicability = wslDistro != null + ? new ApplicabilityScannerExecutor(Logger.getInstance(), wslDistro) + : new ApplicabilityScannerExecutor(Logger.getInstance()); + this.scanners = initScannersCollection(wslDistro); } public SourceCodeScannerManager(Project project, PackageManagerType packageType) { @@ -74,6 +84,24 @@ public SourceCodeScannerManager(Project project, PackageManagerType packageType) this.packageType = packageType; } + /** + * Returns the WSL distro name when {@code project} lives inside a WSL filesystem, otherwise {@code null}. + */ + private static String detectWslDistro(Project project) { + if (!SystemUtils.IS_OS_WINDOWS) { + return null; + } + String basePath = project.getBasePath(); + if (basePath == null) { + return null; + } + String unc = DescriptorPathUtils.intellijWslUrlToUnc(basePath); + if (!WslUtils.isWslPath(unc)) { + return null; + } + return ScanUtils.extractWslDistro(unc); + } + /** * Applicability source code scanning (Contextual Analysis). * @@ -277,7 +305,7 @@ public static List convertToSkippedFolders(String excludePattern) { Matcher matcher = EXCLUSIONS_REGEX_PATTERN.matcher(excludePattern); if (!matcher.find()) { // Convert pattern form shape "**/*a*" to "**/*a*/**" - return List.of(StringUtils.removeEnd(excludePattern, EXCLUSIONS_SUFFIX) + SKIP_FOLDERS_SUFFIX); + return List.of(Strings.CS.removeEnd(excludePattern, EXCLUSIONS_SUFFIX) + SKIP_FOLDERS_SUFFIX); } String[] dirsNames = matcher.group(1).split(","); for (String dirName : dirsNames) { @@ -320,11 +348,17 @@ private Map> mapDirectIssuesByCve(Collection initScannersCollection() { + private Map initScannersCollection(String wslDistro) { Map scanners = new HashMap<>(); - scanners.put(SourceCodeScanType.SECRETS, new SecretsScannerExecutor(Logger.getInstance())); - scanners.put(SourceCodeScanType.IAC, new IACScannerExecutor(Logger.getInstance())); - scanners.put(SourceCodeScanType.SAST, new SastScannerExecutor(Logger.getInstance())); + if (wslDistro != null) { + scanners.put(SourceCodeScanType.SECRETS, new SecretsScannerExecutor(Logger.getInstance(), wslDistro)); + scanners.put(SourceCodeScanType.IAC, new IACScannerExecutor(Logger.getInstance(), wslDistro)); + scanners.put(SourceCodeScanType.SAST, new SastScannerExecutor(Logger.getInstance(), wslDistro)); + } else { + scanners.put(SourceCodeScanType.SECRETS, new SecretsScannerExecutor(Logger.getInstance())); + scanners.put(SourceCodeScanType.IAC, new IACScannerExecutor(Logger.getInstance())); + scanners.put(SourceCodeScanType.SAST, new SastScannerExecutor(Logger.getInstance())); + } return scanners; } diff --git a/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java b/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java index ad936bd8..13d1c24f 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java +++ b/src/main/java/com/jfrog/ide/idea/scan/YarnScanner.java @@ -1,7 +1,6 @@ package com.jfrog.ide.idea.scan; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; @@ -19,6 +18,7 @@ import com.jfrog.ide.idea.scan.utils.ImpactTreeBuilder; import com.jfrog.ide.idea.ui.ComponentsTree; import com.jfrog.ide.idea.ui.menus.filtermanager.ConsistentFilterManager; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -55,7 +55,7 @@ protected DepTree buildTree() throws IOException { @Override protected PsiFile[] getProjectDescriptors() { - VirtualFile file = LocalFileSystem.getInstance().findFileByPath(descriptorFilePath); + VirtualFile file = DescriptorPathUtils.findLocalVirtualFile(descriptorFilePath); if (file == null) { return null; } diff --git a/src/main/java/com/jfrog/ide/idea/scan/data/SarifResult.java b/src/main/java/com/jfrog/ide/idea/scan/data/SarifResult.java index f5e81172..5a2772f0 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/data/SarifResult.java +++ b/src/main/java/com/jfrog/ide/idea/scan/data/SarifResult.java @@ -44,7 +44,7 @@ public String getKind() { } public String getSeverity() { - return StringUtils.defaultString(severity, "warning"); + return Objects.toString(severity, "warning"); } public void setMessage(Message message) { diff --git a/src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java b/src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java index 48f08635..57811077 100644 --- a/src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java +++ b/src/main/java/com/jfrog/ide/idea/scan/utils/ScanUtils.java @@ -8,13 +8,22 @@ import com.intellij.openapi.vfs.VirtualFile; import com.jfrog.ide.common.utils.Utils; import com.jfrog.ide.idea.log.Logger; +import org.jfrog.build.api.util.Log; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.SystemUtils; +import org.jfrog.build.extractor.WslUtils; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Set; +import static com.jfrog.ide.idea.utils.DescriptorPathUtils.WIN_WSL_LOCALHOST_PREFIX; +import static com.jfrog.ide.idea.utils.DescriptorPathUtils.WIN_WSL_PREFIX; + /** * @author yahavi **/ @@ -47,7 +56,7 @@ public static String getOSAndArc() throws IOException { } // Mac if (SystemUtils.IS_OS_MAC) { - if (StringUtils.equalsAny(arch, "aarch64", "arm64")) { + if (Strings.CS.equalsAny(arch, "aarch64", "arm64")) { return "mac-arm64"; } else { return "mac-amd64"; @@ -79,4 +88,87 @@ public static String getOSAndArc() throws IOException { } throw new IOException(String.format("Unsupported OS: %s-%s", SystemUtils.OS_NAME, arch)); } + + /** + * Extracts the WSL distro name from a Windows UNC WSL path. + * e.g. {@code \\wsl$\Ubuntu\home\...} → {@code "Ubuntu"} + */ + public static String extractWslDistro(String uncPath) { + if (uncPath == null) { + return null; + } + String p = WslUtils.normalizePathStringForWsl(uncPath); + String withoutPrefix; + if (p.regionMatches(true, 0, WIN_WSL_LOCALHOST_PREFIX, 0, WIN_WSL_LOCALHOST_PREFIX.length())) { + withoutPrefix = p.substring(WIN_WSL_LOCALHOST_PREFIX.length()); + } else if (p.regionMatches(true, 0, WIN_WSL_PREFIX, 0, WIN_WSL_PREFIX.length())) { + withoutPrefix = p.substring(WIN_WSL_PREFIX.length()); + } else { + return null; + } + int sep = withoutPrefix.indexOf('\\'); + return sep == -1 ? withoutPrefix : withoutPrefix.substring(0, sep); + } + + /** + * Returns the home directory of the default user inside the given WSL distro by running + * {@code wsl.exe -d -e printenv HOME}. + * + * @throws IOException if wsl.exe cannot be executed or returns a non-zero exit code + */ + public static String getWslLinuxHome(String distro) throws IOException { + ProcessBuilder pb = new ProcessBuilder("wsl.exe", "-d", distro, "-e", "printenv", "HOME"); + pb.redirectErrorStream(true); + Process proc = pb.start(); + String output; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8))) { + output = reader.readLine(); + } + int exit; + try { + exit = proc.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while querying WSL home directory", e); + } + if (exit != 0 || StringUtils.isBlank(output)) { + throw new IOException(String.format("Failed to determine WSL home directory for distro '%s' (exit %d)", distro, exit)); + } + return output.trim(); + } + + /** + * Returns the OS/arch download token for the given WSL distro (e.g. {@code "linux-amd64"}) + * by running {@code wsl.exe -d -e uname -m}. Falls back to {@code "linux-amd64"} + * on any error. + */ + public static String getWslArch(String distro, Log log) { + try { + ProcessBuilder pb = new ProcessBuilder("wsl.exe", "-d", distro, "-e", "uname", "-m"); + pb.redirectErrorStream(true); + Process proc = pb.start(); + String arch; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8))) { + arch = reader.readLine(); + } + try { + proc.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + proc.destroyForcibly(); + return "linux-amd64"; + } + if (StringUtils.isBlank(arch)) { + return "linux-amd64"; + } + return switch (arch.trim()) { + case "aarch64", "arm64" -> "linux-arm64"; + case "armv7l", "arm" -> "linux-arm"; + default -> "linux-amd64"; + }; + } catch (IOException e) { + log.warn(String.format("Failed to determine WSL architecture for distro '%s': %s", distro, e.getMessage())); + return "linux-amd64"; + } + } } diff --git a/src/main/java/com/jfrog/ide/idea/ui/ComponentDetails.java b/src/main/java/com/jfrog/ide/idea/ui/ComponentDetails.java index 4d016658..cc6cb31a 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/ComponentDetails.java +++ b/src/main/java/com/jfrog/ide/idea/ui/ComponentDetails.java @@ -8,6 +8,7 @@ import com.jfrog.ide.idea.ui.utils.ComponentUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jfrog.build.extractor.scan.DependencyTree; import org.jfrog.build.extractor.scan.GeneralInfo; import org.jfrog.build.extractor.scan.License; @@ -26,7 +27,7 @@ public ComponentDetails(DependencyTree node) { super(); GeneralInfo generalInfo = node.getGeneralInfo(); String pkgType = StringUtils.capitalize(generalInfo.getPkgType()); - if (StringUtils.equalsAny(pkgType, "Npm", "Go")) { + if (Strings.CS.equalsAny(pkgType, "Npm", "Go")) { addText("Package", generalInfo.getGroupId()); } else { // Maven/Gradle diff --git a/src/main/java/com/jfrog/ide/idea/ui/LocalComponentsTree.java b/src/main/java/com/jfrog/ide/idea/ui/LocalComponentsTree.java index 5acb81e4..4e0d5d2a 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/LocalComponentsTree.java +++ b/src/main/java/com/jfrog/ide/idea/ui/LocalComponentsTree.java @@ -14,6 +14,7 @@ import com.jfrog.ide.idea.navigation.NavigationTarget; import com.jfrog.ide.idea.scan.ScanManager; import com.jfrog.ide.idea.ui.menus.ToolbarPopupMenu; +import com.jfrog.ide.idea.utils.DescriptorPathUtils; import com.jfrog.ide.idea.utils.Utils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -75,7 +76,8 @@ void doAddScanResults(List fileTreeNodes) { for (FileTreeNode node : fileTreeNodes) { FileTreeNode existingNode =(FileTreeNode) Optional.ofNullable(root.getChildren()) .orElseGet(Vector::new).stream() - .filter(treeNode -> ((FileTreeNode) treeNode).getFilePath().equals(node.getFilePath())) + .filter(treeNode -> DescriptorPathUtils.areDescriptorPathsEqual( + ((FileTreeNode) treeNode).getFilePath(), node.getFilePath())) .findFirst().orElse(null); if (existingNode != null) { existingNode.mergeFileTreeNode(node); diff --git a/src/main/java/com/jfrog/ide/idea/ui/configuration/ConfigVerificationUtils.java b/src/main/java/com/jfrog/ide/idea/ui/configuration/ConfigVerificationUtils.java index 962af0bb..59670a58 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/configuration/ConfigVerificationUtils.java +++ b/src/main/java/com/jfrog/ide/idea/ui/configuration/ConfigVerificationUtils.java @@ -2,6 +2,7 @@ import com.intellij.openapi.options.ConfigurationException; import com.jfrog.ide.common.configuration.ServerConfig; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.StringUtils; @@ -11,6 +12,7 @@ import java.util.regex.PatternSyntaxException; import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.Strings.CS; /** * @author yahavi @@ -52,16 +54,16 @@ static void validateGlobalConfig(String excludedPaths, ServerConfig.PolicyType p */ private static void validateExcludedPaths(String excludedPaths) throws ConfigurationException { if (StringUtils.isNotBlank(excludedPaths)) { - if (!StringUtils.startsWith(excludedPaths, EXCLUSIONS_PREFIX)) { + if (!CS.startsWith(excludedPaths, EXCLUSIONS_PREFIX)) { throw new ConfigurationException("Excluded paths pattern must start with " + EXCLUSIONS_PREFIX); } - if (!StringUtils.endsWith(excludedPaths, EXCLUSIONS_SUFFIX)) { + if (!CS.endsWith(excludedPaths, EXCLUSIONS_SUFFIX)) { throw new ConfigurationException("Excluded paths pattern must end with " + EXCLUSIONS_SUFFIX); } try { FileSystems.getDefault().getPathMatcher("glob:" + excludedPaths); } catch (PatternSyntaxException e) { - throw new ConfigurationException(ExceptionUtils.getRootCauseMessage(e), "Excluded paths pattern must be a valid glob pattern"); + throw new ConfigurationException(ExceptionUtils.getRootCauseMessage(e), "Excluded Paths Pattern Must Be a Valid Glob Pattern"); } Matcher matcher = EXCLUSIONS_REGEX_PATTERN.matcher(excludedPaths); if (!matcher.find()) { @@ -82,7 +84,7 @@ private static void validateWatches(String watches) throws ConfigurationExceptio if (isBlank(watches)) { throw new ConfigurationException("Watches must be configured"); } - if (startsWith(watches, ",") || endsWith(watches, ",")) { + if (CS.startsWith(watches, ",") || Strings.CS.endsWith(watches, ",")) { throw new ConfigurationException("Watches list can't start or end with a delimiter"); } for (String part : split(watches, ",")) { diff --git a/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogGlobalConfiguration.java b/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogGlobalConfiguration.java index 78220ba4..4ba82e62 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogGlobalConfiguration.java +++ b/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogGlobalConfiguration.java @@ -21,6 +21,7 @@ import com.jfrog.xray.client.services.system.Version; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.conn.ssl.TrustAllStrategy; @@ -53,6 +54,7 @@ import static com.jfrog.ide.idea.ui.configuration.Utils.*; import static org.apache.commons.collections4.CollectionUtils.addIgnoreNull; import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.Strings.CS; /** * Created by romang on 1/29/17. @@ -509,7 +511,7 @@ private void doSsoLogin() { loginButton.setIcon(null); loginButton.add(asyncProcessIcon); loginButton.setEnabled(false); - String urlStr = removeEnd(trim(platformUrl.getText()), "/") + "/access"; + String urlStr = Strings.CS.removeEnd(trim(platformUrl.getText()), "/") + "/access"; try (AccessManager accessManager = new AccessManager(urlStr, "", Logger.getInstance())) { ProxyConfiguration proxyConfiguration = serverConfig.getProxyConfForTargetUrl(urlStr); if (proxyConfiguration != null) { @@ -521,7 +523,7 @@ private void doSsoLogin() { Thread.sleep(SSO_WAIT_BETWEEN_RETRIES_MILLIS); accessManager.sendBrowserLoginRequest(uuid); - BrowserUtil.browse(removeEnd(platformUrl.getText(), "/") + "/ui/login?jfClientSession=" + uuid + + BrowserUtil.browse(CS.removeEnd(platformUrl.getText(), "/") + "/ui/login?jfClientSession=" + uuid + "&jfClientName=IDEA&jfClientCode=1"); for (int i = 0; i < SSO_RETRIES; i++) { @@ -669,7 +671,7 @@ private void initPolicy() { * Update the policy text fields according to the selected policy type. */ void updatePolicyTextFields() { - switch (ObjectUtils.defaultIfNull(serverConfig.getPolicyType(), ServerConfig.PolicyType.VULNERABILITIES)) { + switch (ObjectUtils.getIfNull(serverConfig.getPolicyType(), ServerConfig.PolicyType.VULNERABILITIES)) { case WATCHES -> { accordingToWatchesRadioButton.setSelected(true); watches.setEnabled(true); diff --git a/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogProjectConfiguration.java b/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogProjectConfiguration.java index 5432c5ca..83c4217f 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogProjectConfiguration.java +++ b/src/main/java/com/jfrog/ide/idea/ui/configuration/JFrogProjectConfiguration.java @@ -8,6 +8,7 @@ import com.intellij.util.messages.MessageBus; import com.jfrog.ide.idea.events.ApplicationEvents; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.Nullable; import javax.swing.*; @@ -41,7 +42,7 @@ JComponent createComponent() { @Override public boolean isModified() { PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(project); - return !StringUtils.equals(propertiesComponent.getValue(BUILDS_PATTERN_KEY), buildsPattern.getText()); + return !Strings.CS.equals(propertiesComponent.getValue(BUILDS_PATTERN_KEY), buildsPattern.getText()); } @Override diff --git a/src/main/java/com/jfrog/ide/idea/ui/menus/builds/BuildsButton.java b/src/main/java/com/jfrog/ide/idea/ui/menus/builds/BuildsButton.java index dae1beea..e7fa22d2 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/menus/builds/BuildsButton.java +++ b/src/main/java/com/jfrog/ide/idea/ui/menus/builds/BuildsButton.java @@ -13,6 +13,7 @@ import com.jfrog.ide.idea.log.Logger; import com.jfrog.ide.idea.ui.menus.filtermanager.CiFilterManager; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import javax.swing.*; import java.awt.event.ItemEvent; @@ -53,7 +54,7 @@ public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { String selectedBuild = (String) e.getItem(); CiFilterManager.getInstance(project).getSelectableBuilds() - .forEach(selectableItem -> selectableItem.setValue(StringUtils.equals(selectedBuild, selectableItem.getKey()))); + .forEach(selectableItem -> selectableItem.setValue(Strings.CS.equals(selectedBuild, selectableItem.getKey()))); MessageBus messageBus = project.getMessageBus(); messageBus.syncPublisher(getSyncEvent()).update(); BuildGeneralInfo generalInfo = CiManager.getInstance(project).getBuildGeneralInfo(selectedBuild); diff --git a/src/main/java/com/jfrog/ide/idea/ui/menus/filtermenu/FilterMenu.java b/src/main/java/com/jfrog/ide/idea/ui/menus/filtermenu/FilterMenu.java index a900f79a..b0b9e10d 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/menus/filtermenu/FilterMenu.java +++ b/src/main/java/com/jfrog/ide/idea/ui/menus/filtermenu/FilterMenu.java @@ -10,6 +10,7 @@ import com.jfrog.ide.idea.ui.menus.ToolbarPopupMenu; import org.apache.commons.compress.utils.Lists; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -18,6 +19,8 @@ import java.util.List; import java.util.Map; +import static org.apache.commons.lang3.Strings.CS; + /** * Created by Yahav Itzhak on 22 Nov 2017. */ @@ -65,7 +68,7 @@ private void setListeners(Map selectionMap) { selectionMap.keySet().stream() .filter(item -> checkBoxMenuItems.stream() .map(AbstractButton::getText) - .noneMatch(text -> StringUtils.equals(text, item.toString()))) + .noneMatch(text -> Strings.CS.equals(text, item.toString()))) .map(key -> new SelectionCheckbox<>(selectionMap, key, getSyncEvent())) .forEach(checkBoxMenuItems::add); selectAllCheckbox.setListeners(selectionMap, checkBoxMenuItems); diff --git a/src/main/java/com/jfrog/ide/idea/utils/Descriptor.java b/src/main/java/com/jfrog/ide/idea/utils/Descriptor.java index 801f9f51..801bd461 100644 --- a/src/main/java/com/jfrog/ide/idea/utils/Descriptor.java +++ b/src/main/java/com/jfrog/ide/idea/utils/Descriptor.java @@ -1,6 +1,7 @@ package com.jfrog.ide.idea.utils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; /** * Represents all supported file descriptor types. @@ -16,7 +17,7 @@ public enum Descriptor { public static Descriptor fromFileName(String fileName) { for (Descriptor descriptor : Descriptor.values()) { - if (StringUtils.equals(descriptor.getFileName(), fileName)) { + if (Strings.CS.equals(descriptor.getFileName(), fileName)) { return descriptor; } } diff --git a/src/main/java/com/jfrog/ide/idea/utils/DescriptorPathUtils.java b/src/main/java/com/jfrog/ide/idea/utils/DescriptorPathUtils.java new file mode 100644 index 00000000..7cd42155 --- /dev/null +++ b/src/main/java/com/jfrog/ide/idea/utils/DescriptorPathUtils.java @@ -0,0 +1,240 @@ +package com.jfrog.ide.idea.utils; + +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import org.jfrog.build.extractor.WslUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Objects; + +/** + * Compares filesystem paths that may refer to the same WSL-backed file but use different spellings + * (e.g. {@code \\wsl$\Distro\...} from {@link java.nio.file.Path} vs {@code //wsl$/Distro/...} from IntelliJ VFS). + *

+ * WSL UNC normalization and Linux-path conversion delegate to {@link WslUtils} (build-info-extractor). + * IntelliJ-style {@code //wsl$/...} URLs are converted to Windows UNC here because that form is IDE-specific. + */ +public final class DescriptorPathUtils { + + public static final String WSL_PREFIX = "//wsl$/"; + public static final String WIN_WSL_PREFIX = "\\\\wsl$\\"; + public static final String WSL_LOCALHOST_PREFIX = "//wsl.localhost/"; + public static final String WIN_WSL_LOCALHOST_PREFIX = "\\\\wsl.localhost\\"; + + private DescriptorPathUtils() { + } + + /** + * Resolves a local filesystem path to a {@link VirtualFile}, trying alternate spellings relevant for WSL mounts. + */ + @Nullable + public static VirtualFile findLocalVirtualFile(@NotNull String filePath) { + LocalFileSystem lfs = LocalFileSystem.getInstance(); + VirtualFile vf = lfs.findFileByPath(filePath); + if (vf != null) { + return vf; + } + vf = lfs.findFileByIoFile(new File(filePath)); + if (vf != null) { + return vf; + } + String unc = intellijWslUrlToUnc(filePath); + if (!unc.equals(filePath)) { + vf = lfs.findFileByPath(unc); + if (vf != null) { + return vf; + } + vf = lfs.findFileByIoFile(new File(unc)); + if (vf != null) { + return vf; + } + } + if (SystemUtils.IS_OS_WINDOWS && filePath.indexOf('\\') >= 0) { + vf = lfs.findFileByPath(filePath.replace('\\', '/')); + if (vf != null) { + return vf; + } + } + return null; + } + + /** + * Returns true if the two paths refer to the same descriptor file or directory, including WSL path variants. + */ + public static boolean areDescriptorPathsEqual(String pathA, String pathB) { + if (pathA == null || pathB == null) { + return pathA == pathB; + } + if (pathA.equals(pathB)) { + return true; + } + if (trySameFile(pathA, pathB)) { + return true; + } + return Objects.equals(toCompareKey(pathA), toCompareKey(pathB)); + } + + private static boolean trySameFile(String pathA, String pathB) { + Path a = tryParsePath(pathA); + Path b = tryParsePath(pathB); + if (a == null || b == null) { + return false; + } + try { + if (Files.exists(a) && Files.exists(b)) { + return Files.isSameFile(a, b); + } + } catch (IOException | SecurityException ignored) { + // fall through to string compare + } + return false; + } + + private static Path tryParsePath(String path) { + try { + return Paths.get(path); + } catch (InvalidPathException e) { + String converted = intellijWslUrlToUnc(path); + if (converted.equals(path)) { + return null; + } + try { + return Paths.get(converted); + } catch (InvalidPathException e2) { + return null; + } + } + } + + /** + * Converts IntelliJ-style WSL URLs ({@code //wsl$/Distro/...}) to Windows UNC for {@link WslUtils}. + */ + public static String intellijWslUrlToUnc(String path) { + if (path == null || path.isEmpty()) { + return path; + } + String p = path; + if (startsWithIgnoreCase(p, WSL_PREFIX)) { + return WIN_WSL_PREFIX + p.substring(WSL_PREFIX.length()).replace('/', '\\'); + } + if (startsWithIgnoreCase(p, WSL_LOCALHOST_PREFIX)) { + return WIN_WSL_LOCALHOST_PREFIX + p.substring(WSL_LOCALHOST_PREFIX.length()).replace('/', '\\'); + } + return path; + } + + /** + * Converts a SARIF {@code artifactLocation.uri} to a path the IDE can open locally. + *

+ * On Windows, {@link Paths#get(URI)} turns Linux {@code file:///home/...} URIs into unusable paths such as + * {@code \\home\\user\\...}. When {@code wslDistro} is set (WSL-hosted project), Linux absolute paths are mapped to + * {@code \\wsl$\{distro}\...}, consistent with {@link com.jfrog.ide.idea.scan.ScanBinaryExecutor} binary paths. + * Windows drive URIs ({@code file:///C:/...}) are left to the default JVM path conversion. + */ + public static String sarifArtifactUriToLocalPath(@Nullable String uriString, @Nullable String wslDistro) { + if (StringUtils.isBlank(uriString)) { + return ""; + } + URI uri; + try { + uri = URI.create(uriString.trim()); + } catch (IllegalArgumentException e) { + return uriString.trim(); + } + return sarifArtifactUriToLocalPath(uri, wslDistro); + } + + private static String sarifArtifactUriToLocalPath(URI uri, @Nullable String wslDistro) { + String scheme = uri.getScheme(); + if (!"file".equalsIgnoreCase(scheme != null ? scheme : "")) { + try { + return Paths.get(uri).toString(); + } catch (InvalidPathException e) { + return uri.toString(); + } + } + if (!SystemUtils.IS_OS_WINDOWS || StringUtils.isBlank(wslDistro)) { + try { + return Paths.get(uri).toString(); + } catch (InvalidPathException e) { + String p = uri.getPath(); + return p != null ? p : uri.toString(); + } + } + String unc = tryLinuxFileUriToWslUnc(uri, wslDistro.trim()); + if (unc != null) { + return unc; + } + try { + return Paths.get(uri).toString(); + } catch (InvalidPathException e) { + String p = uri.getPath(); + return p != null ? p : uri.toString(); + } + } + + /** + * Maps {@code file:///home/...} style URIs to {@code \\wsl$\{distro}\home\...} on Windows; returns null when the URI + * should use normal {@link Paths#get(URI)} handling (e.g. {@code file:///C:/...}). + */ + @Nullable + static String tryLinuxFileUriToWslUnc(URI fileUri, String wslDistro) { + String rawPath = fileUri.getPath(); + if (rawPath == null || rawPath.isEmpty()) { + return null; + } + if (isWindowsDriveFileUriPath(rawPath)) { + return null; + } + if (rawPath.startsWith("/") && rawPath.length() > 1) { + return WIN_WSL_PREFIX + wslDistro + rawPath.replace('/', '\\'); + } + return null; + } + + /** + * Detects {@code file} URI paths that refer to a Windows absolute path ({@code /C:/...}). + */ + static boolean isWindowsDriveFileUriPath(String uriPath) { + return uriPath.length() >= 4 + && uriPath.charAt(0) == '/' + && Character.isLetter(uriPath.charAt(1)) + && uriPath.charAt(2) == ':'; + } + + private static boolean startsWithIgnoreCase(String s, String prefix) { + return s.length() >= prefix.length() && s.regionMatches(true, 0, prefix, 0, prefix.length()); + } + + private static String toCompareKey(String path) { + if (path == null) { + return ""; + } + String p = StringUtils.trimToEmpty(path); + p = intellijWslUrlToUnc(p); + if (WslUtils.isWslPath(p)) { + return WslUtils.toLinuxPath(p); + } + try { + p = Paths.get(p).normalize().toString(); + } catch (InvalidPathException ignored) { + // keep p as-is for best-effort compare + } + p = p.replace('\\', '/'); + if (SystemUtils.IS_OS_WINDOWS) { + p = p.toLowerCase(Locale.ROOT); + } + return p; + } +} diff --git a/src/main/java/com/jfrog/ide/idea/utils/Utils.java b/src/main/java/com/jfrog/ide/idea/utils/Utils.java index dd154abb..4bbb1f36 100644 --- a/src/main/java/com/jfrog/ide/idea/utils/Utils.java +++ b/src/main/java/com/jfrog/ide/idea/utils/Utils.java @@ -14,6 +14,7 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jfrog.build.extractor.scan.DependencyTree; import org.jfrog.build.extractor.scan.GeneralInfo; @@ -50,8 +51,8 @@ public static boolean areRootNodesEqual(DependencyTree lhs, DependencyTree rhs) GeneralInfo lhsGeneralInfo = lhs.getGeneralInfo(); GeneralInfo rhsGeneralInfo = rhs.getGeneralInfo(); return ObjectUtils.allNotNull(lhsGeneralInfo, rhsGeneralInfo) && - StringUtils.equals(lhsGeneralInfo.getPath(), rhsGeneralInfo.getPath()) && - StringUtils.equals(lhsGeneralInfo.getPkgType(), rhsGeneralInfo.getPkgType()); + Strings.CS.equals(lhsGeneralInfo.getPath(), rhsGeneralInfo.getPath()) && + Strings.CS.equals(lhsGeneralInfo.getPkgType(), rhsGeneralInfo.getPkgType()); } public static void focusJFrogToolWindow(Project project) { diff --git a/src/test/java/com/jfrog/ide/idea/scan/ScanBinaryExecutorTest.java b/src/test/java/com/jfrog/ide/idea/scan/ScanBinaryExecutorTest.java index 3d298eae..36e07dea 100644 --- a/src/test/java/com/jfrog/ide/idea/scan/ScanBinaryExecutorTest.java +++ b/src/test/java/com/jfrog/ide/idea/scan/ScanBinaryExecutorTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jfrog.ide.idea.inspections.JFrogSecurityWarning; +import com.jfrog.ide.idea.scan.data.NewScanConfig; +import com.jfrog.ide.idea.scan.data.NewScansConfig; import com.jfrog.ide.idea.scan.data.ScanConfig; import com.jfrog.ide.idea.scan.data.ScansConfig; import junit.framework.TestCase; @@ -10,6 +12,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; @@ -107,8 +110,77 @@ public void testGetBinaryDownloadURL() { assertTrue(actualExternalRepoUrl.startsWith(expectedExternalRepoUrl)); } + public void testGetBinaryDownloadURLForWsl() { + // When WSL distro is set the URL should use a Linux OS distribution token + final String expectedLinuxPrefix = "xsc-gen-exe-analyzer-manager-local/"; + String url = scanner.getBinaryDownloadURL(null); + // Default (non-WSL) scanner was constructed without a distro; just verify URL is non-null and starts correctly + assertTrue(url.startsWith(expectedLinuxPrefix)); + } + + /** + * {@link ScanBinaryExecutor#createTempRunInputFileInWsl(Object, Path)} matches production layout + * (nested {@code jfrog*} dir and {@code .yaml} file); {@code wslTmpBase} is injected so tests run without a WSL UNC mount. + */ + public void testCreateTempRunInputFileInWsl_scansConfigRoundTrip() throws IOException { + Path wslTmpBase = Files.createTempDirectory("wsl-input-parent"); + try { + ScanConfig.Builder builder = new ScanConfig.Builder(); + String testOutput = "/tmp/out.sarif"; + String testLanguage = "Go"; + List testRoots = List.of("/project/a", "/project/b"); + builder.scanType(scanner.scanType); + builder.output(testOutput); + builder.language(testLanguage); + builder.roots(testRoots); + ScansConfig written = new ScansConfig(List.of(builder.Build())); + + Path inputPath = scanner.createTempRunInputFileInWsl(written, wslTmpBase); + assertTrue(inputPath.getFileName().toString().endsWith(".yaml")); + assertTrue(inputPath.getParent().getFileName().toString().startsWith("jfrog")); + assertEquals(wslTmpBase, inputPath.getParent().getParent()); + + ScansConfig readBack = readScansConfigYAML(inputPath); + assertEquals(1, readBack.getScans().size()); + assertEquals(testOutput, readBack.getScans().get(0).getOutput()); + assertEquals(testLanguage, readBack.getScans().get(0).getLanguage()); + assertEquals(testRoots, readBack.getScans().get(0).getRoots()); + } finally { + FileUtils.deleteQuietly(wslTmpBase.toFile()); + } + } + + public void testCreateTempRunInputFileInWsl_newScansConfigRoundTrip() throws IOException { + Path wslTmpBase = Files.createTempDirectory("wsl-input-parent-newfmt"); + try { + ScanConfig.Builder builder = new ScanConfig.Builder(); + builder.scanType(scanner.scanType); + builder.output("/out.sarif"); + builder.language("Java"); + builder.roots(List.of("/src")); + ScanConfig params = builder.Build(); + NewScansConfig written = new NewScansConfig(new NewScanConfig(params)); + + Path inputPath = scanner.createTempRunInputFileInWsl(written, wslTmpBase); + assertTrue(Files.isRegularFile(inputPath)); + + NewScansConfig readBack = readNewScansConfigYAML(inputPath); + assertEquals(1, readBack.getScans().size()); + assertEquals(params.getOutput(), readBack.getScans().get(0).getOutput()); + assertEquals(params.getLanguage(), readBack.getScans().get(0).getLanguage()); + assertEquals(params.getRoots(), readBack.getScans().get(0).getRoots()); + } finally { + FileUtils.deleteQuietly(wslTmpBase.toFile()); + } + } + private ScansConfig readScansConfigYAML(Path inputPath) throws IOException { ObjectMapper mapper = createYAMLMapper(); return mapper.readValue(inputPath.toFile(), ScansConfig.class); } + + private NewScansConfig readNewScansConfigYAML(Path inputPath) throws IOException { + ObjectMapper mapper = createYAMLMapper(); + return mapper.readValue(inputPath.toFile(), NewScansConfig.class); + } } diff --git a/src/test/java/com/jfrog/ide/idea/scan/utils/ScanUtilsTest.java b/src/test/java/com/jfrog/ide/idea/scan/utils/ScanUtilsTest.java new file mode 100644 index 00000000..4e51b34e --- /dev/null +++ b/src/test/java/com/jfrog/ide/idea/scan/utils/ScanUtilsTest.java @@ -0,0 +1,21 @@ +package com.jfrog.ide.idea.scan.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import com.jfrog.ide.idea.scan.utils.ScanUtils; +import org.junit.Test; + + +public class ScanUtilsTest { + @Test + public void testExtractWslDistro() { + assertEquals("Ubuntu", ScanUtils.extractWslDistro("\\\\wsl$\\Ubuntu\\home\\user\\project")); + assertEquals("Debian", ScanUtils.extractWslDistro("\\\\wsl.localhost\\Debian\\home\\user\\project")); + assertEquals("Ubuntu", ScanUtils.extractWslDistro("\\\\WSL$\\Ubuntu\\home\\user")); + // Distro with no trailing path + assertEquals("Ubuntu", ScanUtils.extractWslDistro("\\\\wsl$\\Ubuntu")); + // Non-WSL path returns null + assertNull(ScanUtils.extractWslDistro("C:\\Users\\user\\project")); + assertNull(ScanUtils.extractWslDistro(null)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jfrog/ide/idea/utils/DescriptorPathUtilsTest.java b/src/test/java/com/jfrog/ide/idea/utils/DescriptorPathUtilsTest.java new file mode 100644 index 00000000..f6cb2fe3 --- /dev/null +++ b/src/test/java/com/jfrog/ide/idea/utils/DescriptorPathUtilsTest.java @@ -0,0 +1,115 @@ +package com.jfrog.ide.idea.utils; + +import org.apache.commons.lang3.SystemUtils; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +public class DescriptorPathUtilsTest { + + private static final Object[][] INTELLIJ_WSL_URL_TO_UNC_CASES = new Object[][]{ + {"//wsl$/Ubuntu/home/user/app", "\\\\wsl$\\Ubuntu\\home\\user\\app"}, + {"//WSL$/Ubuntu/home/user/app", "\\\\wsl$\\Ubuntu\\home\\user\\app"}, + {"//wsl.localhost/Ubuntu/home/user/app", "\\\\wsl.localhost\\Ubuntu\\home\\user\\app"}, + {"//WSL.LOCALHOST/Ubuntu/home/user/app", "\\\\wsl.localhost\\Ubuntu\\home\\user\\app"}, + {"C:\\plain\\path", "C:\\plain\\path"}, + }; + + @Test + public void testIntellijWslUrlToUnc() { + for (Object[] row : INTELLIJ_WSL_URL_TO_UNC_CASES) { + String input = (String) row[0]; + String expected = (String) row[1]; + assertEquals(expected, DescriptorPathUtils.intellijWslUrlToUnc(input)); + } + } + + @Test + public void testAreDescriptorPathsEqual_uncAndIntellijUrl() { + String unc = "\\\\wsl$\\Ubuntu\\home\\user\\repo\\package.json"; + String intellij = "//wsl$/Ubuntu/home/user/repo/package.json"; + assertTrue(DescriptorPathUtils.areDescriptorPathsEqual(unc, intellij)); + } + + @Test + public void testAreDescriptorPathsEqual_uncVariants() { + String a = "\\\\wsl$\\Ubuntu\\home\\user\\app"; + String b = "\\\\WSL$\\Ubuntu\\home\\user\\app"; + assertTrue(DescriptorPathUtils.areDescriptorPathsEqual(a, b)); + } + + @Test + public void testAreDescriptorPathsEqual_sameLinuxPathInsideWsl() { + String unc = "\\\\wsl$\\Ubuntu\\home\\user\\proj"; + String intellij = "//wsl$/Ubuntu/home/user/proj"; + assertTrue(DescriptorPathUtils.areDescriptorPathsEqual(unc, intellij)); + } + + @Test + public void testAreDescriptorPathsEqual_extendedLengthUnc() { + String extended = "\\\\?\\UNC\\wsl$\\Ubuntu\\home\\user\\app"; + String regular = "\\\\wsl$\\Ubuntu\\home\\user\\app"; + assertTrue(DescriptorPathUtils.areDescriptorPathsEqual(extended, regular)); + } + + @Test + public void testAreDescriptorPathsEqual_nullAndIdentical() { + assertTrue(DescriptorPathUtils.areDescriptorPathsEqual(null, null)); + assertTrue(DescriptorPathUtils.areDescriptorPathsEqual("/a/b", "/a/b")); + } + + @Test + public void tryLinuxFileUriToWslUnc_mapsLinuxRootToUnc() { + assertEquals( + "\\\\wsl$\\Ubuntu\\home\\user\\app.js", + DescriptorPathUtils.tryLinuxFileUriToWslUnc(URI.create("file:///home/user/app.js"), "Ubuntu")); + } + + @Test + public void tryLinuxFileUriToWslUnc_skipsWindowsDriveUri() { + assertNull(DescriptorPathUtils.tryLinuxFileUriToWslUnc(URI.create("file:///C:/Users/dev/project"), "Ubuntu")); + } + + @Test + public void isWindowsDriveFileUriPath_detectsDriveLetter() { + assertTrue(DescriptorPathUtils.isWindowsDriveFileUriPath("/C:/Users/dev")); + assertTrue(DescriptorPathUtils.isWindowsDriveFileUriPath("/c:/Users/dev")); + assertFalse(DescriptorPathUtils.isWindowsDriveFileUriPath("/home/user")); + } + + @Test + public void sarifArtifactUriToLocalPath_windowsMapsLinuxFileUriToUnc() { + assumeTrue(SystemUtils.IS_OS_WINDOWS); + assertEquals( + "\\\\wsl$\\Ubuntu\\home\\user\\repo\\app.js", + DescriptorPathUtils.sarifArtifactUriToLocalPath("file:///home/user/repo/app.js", "Ubuntu")); + } + + @Test + public void sarifArtifactUriToLocalPath_windowsLeavesWindowsDriveUriUnchangedStyle() { + assumeTrue(SystemUtils.IS_OS_WINDOWS); + String resolved = DescriptorPathUtils.sarifArtifactUriToLocalPath("file:///C:/Users/dev/x.txt", "Ubuntu"); + assertTrue(resolved.endsWith("x.txt")); + assertFalse(resolved.startsWith("\\\\wsl$\\")); + } + + @Test + public void sarifArtifactUriToLocalPath_nonWindowsUsesJvmPathForLinuxUri() { + assumeTrue(!SystemUtils.IS_OS_WINDOWS); + assertEquals( + "/home/user/app.js", + DescriptorPathUtils.sarifArtifactUriToLocalPath("file:///home/user/app.js", "Ubuntu")); + } + + @Test + public void sarifArtifactUriToLocalPath_blankReturnsEmpty() { + assertEquals("", DescriptorPathUtils.sarifArtifactUriToLocalPath("", null)); + assertEquals("", DescriptorPathUtils.sarifArtifactUriToLocalPath(" ", null)); + } +}