diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileInclusionFilter.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileInclusionFilter.java
new file mode 100644
index 0000000000..c938a3ddb7
--- /dev/null
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileInclusionFilter.java
@@ -0,0 +1,76 @@
+package io.jenkins.plugins.analysis.core.filter;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+import org.jenkinsci.Symbol;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+import hudson.Extension;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Descriptor;
+
+/**
+ * A filter that restricts static analysis issues to only those contained within a specific list of files.
+ *
+ *
+ * This is particularly useful for CI pipelines where you want to report issues only for files
+ * changed in a specific Git patch, avoiding the character limits associated with long
+ * regular expression strings.
+ *
+ *
+ * @author Your Name
+ */
+public class FileInclusionFilter extends AbstractDescribableImpl implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1643462711241633469L;
+
+ /**
+ * The path to the text file containing the list of files to be included.
+ * Each line in this file should represent a relative or absolute path
+ * to a file that is allowed to have reported issues.
+ */
+ private final String fileName;
+
+ @Override
+ public Descriptor getDescriptor() {
+ return new DescriptorImpl();
+ }
+
+ /**
+ * Creates a new instance of {@link FileInclusionFilter}.
+ *
+ * @param fileName
+ * the path to the file containing the list of allowed file names (one per line).
+ * Note: If running on a distributed Jenkins setup, this path must be accessible
+ * on the controller or handled via FilePath. A blank or null value is treated as
+ * "no filter" and stored as null.
+ */
+ public FileInclusionFilter(final String fileName) {
+ super();
+ this.fileName = fileName != null && !fileName.isBlank() ? fileName : null;
+ }
+
+ /**
+ * Returns the path to the file containing the inclusion list.
+ *
+ * @return the file name
+ */
+ public String getFileName() {
+ return fileName;
+ }
+
+ /**
+ * Descriptor for {@link FileInclusionFilter}.
+ */
+ @Extension
+ @Symbol("fileInclusionFilter")
+ public static class DescriptorImpl extends Descriptor {
+ @Override
+ @NonNull
+ public String getDisplayName() {
+ return "Include only files listed in file";
+ }
+ }
+}
diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileNameFilter.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileNameFilter.java
new file mode 100644
index 0000000000..f92fb7f7b7
--- /dev/null
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileNameFilter.java
@@ -0,0 +1,60 @@
+package io.jenkins.plugins.analysis.core.filter;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import edu.hm.hafner.analysis.Issue;
+
+/**
+ * A {@link Predicate} that filters {@link Issue} instances by checking their file names against
+ * a provided collection of allowed paths.
+ *
+ *
+ * This filter is designed to efficiently handle the intersection between the absolute paths
+ * often reported by static analysis tools and the relative paths typically generated by
+ * SCM tools (like {@code git diff}). It uses an "ends-with" matching strategy to bridge
+ * these differences.
+ *
+ *
+ * @author Your Name
+ */
+public class FileNameFilter implements Predicate {
+ /** The set of file paths that are permitted to remain in the report. */
+ private final Set allowedFiles;
+
+ /**
+ * Creates a new instance of {@link FileNameFilter}.
+ *
+ * @param fileList
+ * the collection of file paths to include in the filter. These are typically
+ * retrieved from a version control system's diff output.
+ */
+ public FileNameFilter(final Collection fileList) {
+ this.allowedFiles = fileList.stream().collect(Collectors.toSet());
+ }
+
+ /**
+ * Evaluates this predicate on the given issue.
+ *
+ *
+ * An issue passes the filter if its file name ends with any of the strings in the
+ * allowed files set. This ensures that an issue at {@code /absolute/path/to/src/File.ts}
+ * matches an allowed entry of {@code src/File.ts}.
+ *
+ *
+ * @param issue
+ * the issue to test.
+ * @return {@code true} if the issue's file name is allowed, {@code false} otherwise.
+ */
+ @Override
+ public boolean test(final Issue issue) {
+ String fileName = issue.getFileName();
+
+ // Match if the issue's path ends with any file in our diff list.
+ // Stream search is used here; for extremely large sets, a suffix-tree
+ // approach could be used to further optimize if necessary.
+ return allowedFiles.stream().anyMatch(fileName::endsWith);
+ }
+}
diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FilterConfig.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FilterConfig.java
new file mode 100644
index 0000000000..9935f987d0
--- /dev/null
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FilterConfig.java
@@ -0,0 +1,80 @@
+package io.jenkins.plugins.analysis.core.filter;
+
+import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.io.Serializable;
+import java.util.List;
+import java.util.logging.Logger;
+
+import hudson.FilePath;
+
+/**
+ * Bundles filter configuration for issue scanning. Groups regex-based filters and an optional
+ * file-based inclusion filter into a single parameter object.
+ *
+ * @param filters the list of regular expression filters to apply to issues
+ * @param filesFilter an optional path to a file that lists the files to include
+ */
+public record FilterConfig(
+ List filters,
+ String filesFilter) implements Serializable {
+ private static final Logger LOGGER = Logger.getLogger(FilterConfig.class.getName());
+
+ /**
+ * Reads the file inclusion list from the workspace and creates a {@link FileNameFilter}.
+ *
+ * @param workspace
+ * the workspace path used to resolve the filter file location
+ * @return a {@link FileNameFilter} if the filter file is configured and readable,
+ * {@code null} otherwise
+ */
+ public FileNameFilter readFileNameFilter(final FilePath workspace) {
+ if (filesFilter == null || workspace == null || filesFilter.isBlank()) {
+ return null;
+ }
+
+ // Security: Paths.get().normalize() resolves ".." and "." traversal components.
+ // CodeQL recognizes this as a taint sanitizer for path-injection, breaking the
+ // taint chain from the user-controlled filesFilter. After normalization, we verify
+ // the result is not absolute and does not start with ".." (which would indicate
+ // traversal outside the base). Finally, isDescendant() provides defense-in-depth.
+ Path normalized;
+ try {
+ normalized = Paths.get(filesFilter).normalize();
+ }
+ catch (InvalidPathException e) {
+ LOGGER.warning(() -> String.format(
+ "Rejected invalid filter file path in plugin configuration: '%s'", filesFilter));
+ return null;
+ }
+
+ if (normalized.isAbsolute() || normalized.startsWith("..")) {
+ LOGGER.warning(() -> String.format(
+ "Rejected unsafe filter file path in plugin configuration: '%s'", filesFilter));
+ return null;
+ }
+
+ String safePath = normalized.toString();
+
+ try {
+ FilePath fileFilterPath = workspace.child(safePath);
+ if (!fileFilterPath.isDescendant(workspace.getRemote())) {
+ LOGGER.warning(() -> String.format(
+ "Blocked potential path traversal attempt in plugin configuration. Target path '%s' is outside of workspace '%s'",
+ filesFilter, workspace.getRemote()
+ ));
+ return null;
+ }
+
+ String content = fileFilterPath.readToString();
+ List lines = Arrays.asList(content.split("\\r?\\n"));
+ return new FileNameFilter(lines);
+ }
+ catch (IOException | InterruptedException e) {
+ return null;
+ }
+ }
+}
diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java
index 628e3f5705..2b9b24a04e 100644
--- a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java
@@ -45,7 +45,7 @@
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
-
+import io.jenkins.plugins.analysis.core.filter.FilterConfig;
import io.jenkins.plugins.analysis.core.filter.RegexpFilter;
import io.jenkins.plugins.analysis.core.model.AnalysisResult;
import io.jenkins.plugins.analysis.core.model.HealthReportBuilder;
@@ -111,6 +111,7 @@ public class IssuesRecorder extends Recorder {
private Severity minimumSeverity = Severity.WARNING_LOW;
private List filters = new ArrayList<>();
+ private String filesFilter;
private boolean isEnabledForFailure;
private boolean isAggregatingResults;
@@ -689,6 +690,21 @@ public void setFilters(final List filters) {
this.filters = new ArrayList<>(filters);
}
+ public String getFilesFilter() {
+ return this.filesFilter;
+ }
+
+ /**
+ * Sets the file path to read the files that should be included in the recording.
+ *
+ * @param filePath
+ * the path to the file listing the files to include
+ */
+ @DataBoundSetter
+ public void setFilesFilter(final String filePath) {
+ this.filesFilter = filePath;
+ }
+
public void setChecksInfo(@CheckForNull final ChecksInfo checksInfo) {
this.checksInfo = checksInfo;
}
@@ -825,7 +841,8 @@ private String getReportName(final Tool tool) {
private AnnotatedReport scanWithTool(final Run, ?> run, final FilePath workspace, final TaskListener listener,
final Tool tool) throws IOException, InterruptedException {
- var issuesScanner = new IssuesScanner(tool, getFilters(), getSourceCodeCharset(),
+ var filterConfig = new FilterConfig(getFilters(), filesFilter);
+ var issuesScanner = new IssuesScanner(tool, filterConfig, getSourceCodeCharset(),
workspace, getSourceCodePaths(), getSourceCodeRetention(),
run, new FilePath(run.getRootDir()), listener,
scm, isBlameDisabled ? BlameMode.DISABLED : BlameMode.ENABLED,
diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java
index 383e9a4ac2..1e233acdad 100644
--- a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesScanner.java
@@ -22,7 +22,6 @@
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@@ -33,7 +32,7 @@
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import jenkins.MasterToSlaveFileCallable;
-
+import io.jenkins.plugins.analysis.core.filter.FilterConfig;
import io.jenkins.plugins.analysis.core.filter.RegexpFilter;
import io.jenkins.plugins.analysis.core.model.ReportLocations;
import io.jenkins.plugins.analysis.core.model.ReportScanningTool;
@@ -70,7 +69,7 @@ class IssuesScanner {
private final FilePath jenkinsRootDir;
private final Charset sourceCodeEncoding;
private final Tool tool;
- private final List filters;
+ private final FilterConfig filterConfig;
private final TaskListener listener;
private final String scm;
private final BlameMode blameMode;
@@ -88,13 +87,14 @@ enum PostProcessingMode {
}
@SuppressWarnings("checkstyle:ParameterNumber")
- IssuesScanner(final Tool tool, final List filters, final Charset sourceCodeEncoding,
+ IssuesScanner(final Tool tool, final FilterConfig filterConfig,
+ final Charset sourceCodeEncoding,
final FilePath workspace, final Set sourceDirectories,
final SourceCodeRetention sourceCodeRetention, final Run, ?> run,
final FilePath jenkinsRootDir, final TaskListener listener,
final String scm, final BlameMode blameMode, final PostProcessingMode postProcessingMode,
final boolean quiet, final String sourcePathPrefix, final String targetPathPrefix) {
- this.filters = new ArrayList<>(filters);
+ this.filterConfig = filterConfig;
this.sourceCodeEncoding = sourceCodeEncoding;
this.tool = tool;
this.workspace = workspace;
@@ -146,7 +146,7 @@ private AnnotatedReport postProcessReport(final Report report) throws IOExceptio
}
else {
report.logInfo("Skipping post processing");
- return new AnnotatedReport(tool.getActualId(), filter(report, filters));
+ return new AnnotatedReport(tool.getActualId(), filter(report, filterConfig, workspace));
}
}
@@ -156,7 +156,7 @@ private ReportPostProcessor createPostProcessor(final Report report) {
linesLookAhead = scanningTool.getLinesLookAhead();
}
return new ReportPostProcessor(tool.getActualId(), report, sourceCodeEncoding.name(),
- createBlamer(report), filters, getPermittedSourceDirectories(), sourceDirectories,
+ createBlamer(report), filterConfig, getPermittedSourceDirectories(), sourceDirectories,
postProcessingMode, linesLookAhead, sourcePathPrefix, targetPathPrefix);
}
@@ -230,30 +230,42 @@ private String getComputerName() {
return StringUtils.EMPTY;
}
- private static Report filter(final Report report, final List filters) {
+ private static Report filter(final Report report, final FilterConfig filterConfig,
+ final FilePath workspace) {
int actualFilterSize = 0;
var builder = new IssueFilterBuilder();
- for (RegexpFilter filter : filters) {
+ for (RegexpFilter filter : filterConfig.filters()) {
if (StringUtils.isNotBlank(filter.getPattern())) {
filter.apply(builder);
actualFilterSize++;
}
}
+
var filtered = report.filter(builder.build());
+ var result = filtered;
+
+ var fileNameFilter = filterConfig.readFileNameFilter(workspace);
+ if (fileNameFilter != null) {
+ result = filtered.filter(fileNameFilter);
+ actualFilterSize++;
+ }
+
if (actualFilterSize > 0) {
filtered.logInfo(
"Applying %d filters on the set of %d issues (%d issues have been removed, %d issues will be published)",
- filters.size(), report.size(), report.size() - filtered.size(), filtered.size());
+ actualFilterSize, report.size(), report.size() - filtered.size(), result.size());
}
else {
- filtered.logInfo("No filter has been set, publishing all %d issues", filtered.size());
+ filtered.logInfo("No filter has been set, publishing all %d issues", result.size());
}
- return filtered;
+ return result;
}
/**
- * Post processes the report on the build agent. Assigns absolute paths, package names, and module names and
- * computes fingerprints for each issue. Finally, for each file the SCM blames are computed.
+ * Post processes the report on the build agent. Assigns absolute paths, package
+ * names, and module names and
+ * computes fingerprints for each issue. Finally, for each file the SCM blames
+ * are computed.
*/
@SuppressWarnings("checkstyle:ClassDataAbstractionCoupling")
private static class ReportPostProcessor extends MasterToSlaveFileCallable {
@@ -270,23 +282,25 @@ private static class ReportPostProcessor extends MasterToSlaveFileCallable requestedSourceDirectories;
private final PostProcessingMode postProcessingMode;
@SuppressWarnings("serial")
- private final List filters;
+ private final FilterConfig filterConfig;
private final int linesLookAhead;
private final String sourcePathPrefix;
private final String targetPathPrefix;
@SuppressWarnings("checkstyle:ParameterNumber")
ReportPostProcessor(final String id, final Report report, final String sourceCodeEncoding,
- final Blamer blamer, final List filters, final Set permittedSourceDirectories,
- final Set requestedSourceDirectories, final PostProcessingMode postProcessingMode, final int linesLookAhead,
- final String sourcePathPrefix, final String targetPathPrefix) {
+ final Blamer blamer, final FilterConfig filterConfig,
+ final Set permittedSourceDirectories,
+ final Set requestedSourceDirectories, final PostProcessingMode postProcessingMode,
+ final int linesLookAhead,
+ final String sourcePathPrefix, final String targetPathPrefix) {
super();
this.id = id;
originalReport = report;
this.sourceCodeEncoding = sourceCodeEncoding;
this.blamer = blamer;
- this.filters = filters;
+ this.filterConfig = filterConfig;
this.permittedSourceDirectories = permittedSourceDirectories;
this.requestedSourceDirectories = requestedSourceDirectories;
this.postProcessingMode = postProcessingMode;
@@ -306,7 +320,7 @@ public AnnotatedReport invoke(final File workspace, final VirtualChannel channel
originalReport.logInfo(SKIPPING_POST_PROCESSING);
}
- Report filtered = filter(originalReport, filters); // the filters may depend on the resolved paths
+ Report filtered = filter(originalReport, filterConfig, new FilePath(workspace)); // the filters may depend on the resolved paths
createFingerprints(filtered);
diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java
index 814f776fe4..276b7cc4dd 100644
--- a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java
@@ -29,7 +29,7 @@
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
-
+import io.jenkins.plugins.analysis.core.filter.FileInclusionFilter;
import io.jenkins.plugins.analysis.core.filter.RegexpFilter;
import io.jenkins.plugins.analysis.core.model.AnalysisResult;
import io.jenkins.plugins.analysis.core.model.HealthReportBuilder;
@@ -83,6 +83,7 @@ public class RecordIssuesStep extends Step implements Serializable {
@SuppressWarnings("serial")
private List filters = new ArrayList<>();
+ private FileInclusionFilter filesFilter;
private boolean isEnabledForFailure;
private boolean isAggregatingResults;
@@ -638,6 +639,15 @@ public void setFilters(final List filters) {
this.filters = new ArrayList<>(filters);
}
+ public String getFilesFilter() {
+ return filesFilter != null ? filesFilter.getFileName() : null;
+ }
+
+ @DataBoundSetter
+ public void setFilesFilter(final String filePath) {
+ this.filesFilter = new FileInclusionFilter(filePath);
+ }
+
@Override
public StepExecution start(final StepContext context) {
return new Execution(context, this);
@@ -669,6 +679,7 @@ protected List run() throws IOException, InterruptedException {
recorder.setUnhealthy(step.getUnhealthy());
recorder.setMinimumSeverity(step.getMinimumSeverity());
recorder.setFilters(step.getFilters());
+ recorder.setFilesFilter(step.getFilesFilter());
recorder.setEnabledForFailure(step.getEnabledForFailure());
recorder.setAggregatingResults(step.getAggregatingResults());
recorder.setBlameDisabled(step.isSkipBlames());
diff --git a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java
index 0515791dbe..fe9c486576 100644
--- a/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java
+++ b/plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/ScanForIssuesStep.java
@@ -24,7 +24,8 @@
import hudson.FilePath;
import hudson.model.Run;
import hudson.model.TaskListener;
-
+import io.jenkins.plugins.analysis.core.filter.FileInclusionFilter;
+import io.jenkins.plugins.analysis.core.filter.FilterConfig;
import io.jenkins.plugins.analysis.core.filter.RegexpFilter;
import io.jenkins.plugins.analysis.core.model.Tool;
import io.jenkins.plugins.analysis.core.steps.IssuesScanner.BlameMode;
@@ -47,6 +48,7 @@ public class ScanForIssuesStep extends Step {
private boolean quiet;
private List filters = new ArrayList<>();
+ private FileInclusionFilter filesFilter;
private String scm = StringUtils.EMPTY;
private String sourcePathPrefix = StringUtils.EMPTY; // @since 10.7.0
@@ -87,6 +89,15 @@ public void setFilters(final List filters) {
this.filters = new ArrayList<>(filters);
}
+ public String getFilesFilter() {
+ return filesFilter != null ? filesFilter.getFileName() : null;
+ }
+
+ @DataBoundSetter
+ public void setFileFilters(final String filePath) {
+ this.filesFilter = new FileInclusionFilter(filePath);
+ }
+
/**
* Sets whether logging output should be enabled.
*
@@ -274,7 +285,7 @@ static class Execution extends AnalysisExecution {
private final boolean isBlameDisabled;
private final boolean skipPostProcessing;
@SuppressWarnings("serial")
- private final List filters;
+ private final FilterConfig filterConfig;
@SuppressWarnings("serial")
private final Set sourceDirectories;
private final String scm;
@@ -297,7 +308,7 @@ static class Execution extends AnalysisExecution {
tool = step.getTool();
sourceCodeEncoding = step.getSourceCodeEncoding();
isBlameDisabled = step.isSkipBlames();
- filters = step.getFilters();
+ filterConfig = new FilterConfig(step.getFilters(), step.getFilesFilter());
sourceDirectories = step.getAllSourceDirectories();
sourceCodeRetention = step.getSourceCodeRetention();
scm = step.getScm();
@@ -312,7 +323,7 @@ protected AnnotatedReport run() throws IOException, InterruptedException, Illega
var workspace = getWorkspace();
var listener = getTaskListener();
- var issuesScanner = new IssuesScanner(tool, filters,
+ var issuesScanner = new IssuesScanner(tool, filterConfig,
getCharset(sourceCodeEncoding), workspace, sourceDirectories,
sourceCodeRetention, getRun(), new FilePath(getRun().getRootDir()), listener,
scm, isBlameDisabled ? BlameMode.DISABLED : BlameMode.ENABLED,
diff --git a/plugin/src/main/resources/issues/scan-parameters.jelly b/plugin/src/main/resources/issues/scan-parameters.jelly
index 44d6ed3c9b..6a6571ae3e 100644
--- a/plugin/src/main/resources/issues/scan-parameters.jelly
+++ b/plugin/src/main/resources/issues/scan-parameters.jelly
@@ -24,6 +24,10 @@
+
+
+
+
diff --git a/plugin/src/main/resources/issues/scan-parameters.properties b/plugin/src/main/resources/issues/scan-parameters.properties
index 46ec1a03a6..d0530cb7af 100644
--- a/plugin/src/main/resources/issues/scan-parameters.properties
+++ b/plugin/src/main/resources/issues/scan-parameters.properties
@@ -6,6 +6,7 @@ description.sourceDirectories=Additional paths to the source code if not in the
title.blameDisabled=Disable retrieval of blame information (author and commit) from SCM
title.skipPostProcessing=Disable detection of missing package and module names
title.filter=Issue Filters
+title.filelist=Filename for list inclusion
description.filter=Issues will be matched with all the specified filters. If no filter is \
defined, then all issues will be published. Filters with empty regular expression will be ignored.
sourceCodeRetention.title=Source Code Retention Strategy
@@ -15,3 +16,4 @@ description.sourcePathPrefix=Path prefix to be replaced in file paths (e.g., pat
title.targetPathPrefix=Target Path Prefix
description.targetPathPrefix=Path prefix to replace with (e.g., path in Jenkins workspace: /var/jenkins/workspace). \
Leave empty to disable path remapping. Both source and target prefixes must be provided for remapping to work.
+description.filename=Specify the filter file to apply. The file should contains the file to be included in the filter.