Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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.
* </p>
*
* @author Your Name
*/
public class FileInclusionFilter extends AbstractDescribableImpl<FileInclusionFilter> implements Serializable {

Check warning on line 25 in plugin/src/main/java/io/jenkins/plugins/analysis/core/filter/FileInclusionFilter.java

View check run for this annotation

ci.jenkins.io / Java Compiler

compiler:compile

NORMAL: hudson.model.AbstractDescribableImpl in hudson.model has been deprecated
@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<FileInclusionFilter> 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<FileInclusionFilter> {
@Override
@NonNull
public String getDisplayName() {
return "Include only files listed in file";
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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.
* </p>
*
* @author Your Name
*/
public class FileNameFilter implements Predicate<Issue> {
/** The set of file paths that are permitted to remain in the report. */
private final Set<String> 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<String> fileList) {
this.allowedFiles = fileList.stream().collect(Collectors.toSet());
}

/**
* Evaluates this predicate on the given issue.
*
* <p>
* 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}.
* </p>
*
* @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);
}
}
Original file line number Diff line number Diff line change
@@ -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<RegexpFilter> 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<String> lines = Arrays.asList(content.split("\\r?\\n"));
return new FileNameFilter(lines);
}
catch (IOException | InterruptedException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +111,7 @@
private Severity minimumSeverity = Severity.WARNING_LOW;

private List<RegexpFilter> filters = new ArrayList<>();
private String filesFilter;

private boolean isEnabledForFailure;
private boolean isAggregatingResults;
Expand Down Expand Up @@ -678,17 +679,32 @@
@DataBoundSetter
public void setMinimumSeverity(final String minimumSeverity) {
this.minimumSeverity = Severity.valueOf(minimumSeverity, Severity.WARNING_LOW);
}

public List<RegexpFilter> getFilters() {
return new ArrayList<>(filters);
}

@DataBoundSetter
public void setFilters(final List<RegexpFilter> filters) {
this.filters = new ArrayList<>(filters);
}

public String getFilesFilter() {
return this.filesFilter;

Check warning on line 694 in plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>} public List&lt;RegexpFilter&gt; getFilters() { return new ArrayList&lt;&gt;(filters); } &#64;DataBoundSetter public void setFilters(final List&lt;RegexpFilter&gt; filters) { this.filters &#61; new ArrayList&lt;&gt;(filters); } public String getFilesFilter() { return this.filesFilter;</code></pre>
}

/**
* 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;
}
Expand Down Expand Up @@ -825,7 +841,8 @@

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,
Expand Down
Loading
Loading