Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ according to your preferences.
- **Proxy Configuration** :
<br >From IntelliJ IDEA Appearance & Behavior > System Settings > HTTP Proxy, you can configure a static proxy for all HTTP requests made by the plugin. This is useful when your environment requires going through a proxy to access external services. For example:`http://proxy.example.com:8080`

- **Manifest Exclusion Patterns** :
<br >You can exclude manifest files from component analysis using glob patterns. This is useful for excluding third-party dependencies, test files, or other manifests that should not be analyzed.
<br >Enter one pattern per line. Examples: `**/node_modules/**/package.json` to exclude all package.json files in node_modules directories, or `test/**/pom.xml` to exclude all Maven files in test directories.

## Features

- **Component analysis**
Expand Down Expand Up @@ -294,6 +298,18 @@ according to your preferences.
You can create an alternative file to `requirements.txt`, for example, a `requirements-dev.txt` or
a `requirements-test.txt` file where you can add the development or test dependencies there.


- **Excluding manifest files with patterns**
<br >You can exclude specific manifest files from component analysis using configurable glob patterns. This feature allows you to avoid analyzing third-party dependencies, test files, or other manifests that are not relevant to your security analysis.
<br >Patterns are configured in the plugin settings under **Tools > Red Hat Dependency Analytics > Manifest Exclusion Patterns**.
<br >Examples of exclusion patterns:
- `**/node_modules/**/package.json` - Excludes all package.json files in node_modules directories
- `test/**/pom.xml` - Excludes all Maven pom.xml files in test directories
- `vendor/**/*.go.mod` - Excludes all go.mod files in vendor directories
- `**/build.gradle` - Excludes all Gradle build files
<br >Right-click on any manifest file and select **Exclude from Component Analysis** to quickly add an exclusion pattern for that specific file.


- **Red Hat Dependency Analytics report**
<br >The Red Hat Dependency Analytics report is a temporary HTML file that exist if the **Red Hat Dependency Analytics
Report** tab remains open.
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dependencies {

// for tests
testImplementation(libs.junit)
testImplementation(libs.mockito)
}

tasks {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exhort-api-spec = "1.0.18"
exhort-java-api = "0.0.8"
github-api = "1.314"
junit = "4.13.2"
mockito = "4.11.0"
packageurl-java = "1.4.1"

# plugins
Expand All @@ -20,6 +21,7 @@ exhort-api-spec = { group = "com.redhat.ecosystemappeng", name = "exhort-api-spe
exhort-java-api = { group = "com.redhat.exhort", name = "exhort-java-api", version.ref = "exhort-java-api" }
github-api = { group = "org.kohsuke", name = "github-api", version.ref = "github-api" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
packageurl-java = { group = "com.github.package-url", name = "packageurl-java", version.ref = "packageurl-java" }

[plugins]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public abstract class CAAnnotator extends ExternalAnnotator<CAAnnotator.Info, Ma
if (inspection == null) {
return null;
}

if (ManifestExclusionManager.isManifestExcluded(file.getVirtualFile(), file.getProject())) {
LOG.debug("Skipping analysis for excluded manifest: " + file.getName());
return null;
}

LOG.info("Get dependencies");
return new Info(file, this.getDependencies(file));
}
Expand Down Expand Up @@ -177,6 +183,7 @@ public void apply(@NotNull PsiFile file, Map<Dependency, Result> annotationResul
}
}
builder.withFix(new SAIntentionAction());
builder.withFix(new ExcludeManifestIntentionAction());
builder.create();
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@
import com.redhat.exhort.api.v4.DependencyReport;
import com.redhat.exhort.api.v4.ProviderReport;
import com.redhat.exhort.api.v4.Source;
import org.apache.commons.io.FileUtils;
import org.jboss.tools.intellij.exhort.ApiService;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -171,14 +167,4 @@ public static boolean performAnalysis(String packageManager,
}
return false;
}

private static void deleteTempDir(Path tempDirectory) {
try {
FileUtils.deleteDirectory(tempDirectory.toFile());
} catch (IOException e) {
LOG.warn("Failed to delete temp directory: " + tempDirectory, e);
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*******************************************************************************
* Copyright (c) 2025 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/

package org.jboss.tools.intellij.componentanalysis;

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.intention.FileModifier;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class ExcludeManifestIntentionAction implements IntentionAction {

@Override
public @IntentionName @NotNull String getText() {
return "Exclude this manifest from component analysis";
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return "RHDA";
}

@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
if (file == null || file.getVirtualFile() == null) {
return false;
}

String fileName = file.getName();
return "pom.xml".equals(fileName) ||
"package.json".equals(fileName) ||
"go.mod".equals(fileName) ||
"requirements.txt".equals(fileName) ||
"build.gradle".equals(fileName);
}

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) {
return;
}

VirtualFile projectRoot = project.getBaseDir();
if (projectRoot == null) {
return;
}

String filePath = virtualFile.getPath();
String projectPath = projectRoot.getPath();

if (filePath.startsWith(projectPath)) {
String relativePath = filePath.substring(projectPath.length());
if (relativePath.startsWith("/") || relativePath.startsWith("\\")) {
relativePath = relativePath.substring(1);
}

ManifestExclusionManager.addExclusionPattern(relativePath, project);

ApplicationManager.getApplication().runReadAction(() -> {
DaemonCodeAnalyzer.getInstance(project).restart(file);
});
}
}

@Override
public boolean startInWriteAction() {
return false;
}

@Override
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*******************************************************************************
* Copyright (c) 2025 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/

package org.jboss.tools.intellij.componentanalysis;

import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import org.jboss.tools.intellij.settings.ApiSettingsState;

import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class ManifestExclusionManager {

private static final Logger LOG = Logger.getInstance(ManifestExclusionManager.class);

public static boolean isManifestExcluded(VirtualFile file, Project project) {
if (file == null) {
return false;
}

ApiSettingsState settings = ApiSettingsState.getInstance();
String patterns = settings.manifestExclusionPatterns;

if (patterns == null || patterns.trim().isEmpty()) {
return false;
}

VirtualFile projectRoot = project.getBaseDir();
if (projectRoot == null) {
return false;
}

String relativePath = getRelativePath(file, projectRoot);
if (relativePath == null) {
return false;
}

List<String> exclusionPatterns = parsePatterns(patterns);
return matchesAnyPattern(relativePath, exclusionPatterns);
}

public static void addExclusionPattern(String manifestPath, Project project) {
if (manifestPath == null || manifestPath.trim().isEmpty()) {
return;
}

VirtualFile projectRoot = project.getBaseDir();
if (projectRoot == null) {
return;
}

VirtualFile manifestFile = project.getBaseDir().findFileByRelativePath(manifestPath);
if (manifestFile == null) {
return;
}

String relativePath = getRelativePath(manifestFile, projectRoot);
if (relativePath == null) {
return;
}

ApiSettingsState settings = ApiSettingsState.getInstance();
List<String> currentPatterns = parsePatterns(settings.manifestExclusionPatterns);

if (!currentPatterns.contains(relativePath)) {
currentPatterns.add(relativePath);
settings.manifestExclusionPatterns = String.join("\n", currentPatterns);
}
}

private static String getRelativePath(VirtualFile file, VirtualFile projectRoot) {
try {
String filePath = file.getPath();
String projectPath = projectRoot.getPath();

if (filePath.startsWith(projectPath)) {
String relativePath = filePath.substring(projectPath.length());
if (relativePath.startsWith("/") || relativePath.startsWith("\\")) {
relativePath = relativePath.substring(1);
}
return relativePath;
}
} catch (Exception e) {
LOG.warn("Failed to get relative path for file: " + file.getPath(), e);
}
return null;
}

private static List<String> parsePatterns(String patterns) {
if (patterns == null || patterns.trim().isEmpty()) {
return Collections.emptyList();
}

return Arrays.stream(patterns.split("[\n\r]+"))
.map(String::trim)
.filter(pattern -> !pattern.isEmpty() && !pattern.startsWith("#"))
.collect(Collectors.toList());
}

private static boolean matchesAnyPattern(String path, List<String> patterns) {
Path filePath = Paths.get(path);

for (String pattern : patterns) {
try {
// Test the pattern as-is first
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
if (matcher.matches(filePath)) {
LOG.debug("File " + path + " matches exclusion pattern: " + pattern);
return true;
}

// For patterns starting with "**/" that don't match, also test against
// the path with a virtual directory prefix to handle the Java PathMatcher
// limitation where "**/file.txt" doesn't match "file.txt" at root
if (pattern.startsWith("**/")) {
// Try matching with a virtual directory prefix
Path prefixedPath = Paths.get("dummy/" + path);
if (matcher.matches(prefixedPath)) {
LOG.debug("File " + path + " matches exclusion pattern: " + pattern + " (with directory prefix)");
return true;
}
}
} catch (Exception e) {
LOG.warn("Invalid glob pattern: " + pattern, e);
}
}

return false;
}

public static List<String> getExclusionPatterns() {
ApiSettingsState settings = ApiSettingsState.getInstance();
return parsePatterns(settings.manifestExclusionPatterns);
}

public static void setExclusionPatterns(List<String> patterns) {
ApiSettingsState settings = ApiSettingsState.getInstance();
settings.manifestExclusionPatterns = String.join("\n", patterns);
}
}
Loading
Loading