Skip to content

Commit

Permalink
Add file searching ability to CodeDirectory (#363)
Browse files Browse the repository at this point in the history
Analyzing binary result from multiple tools shows we'll need this
functionality in multiple places, so we're adding it to the generally
available `CodeDirectory` type.
  • Loading branch information
nahsra authored Apr 23, 2024
1 parent 86438ca commit 49e4d79
Show file tree
Hide file tree
Showing 18 changed files with 161 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,11 @@ public Integer call() throws IOException {
}

// create the loader
CodeDirectory codeDirectory = new DefaultCodeDirectory(projectPath);
List<Path> sarifFiles =
sarifs != null ? sarifs.stream().map(Path::of).collect(Collectors.toList()) : List.of();
Map<String, List<RuleSarif>> pathSarifMap =
SarifParser.create().parseIntoMap(sarifFiles, projectPath);
SarifParser.create().parseIntoMap(sarifFiles, codeDirectory);
List<ParameterArgument> codemodParameters =
createFromParameterStrings(this.codemodParameters);
CodemodLoader loader =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
package io.codemodder;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;

/** Holds a code directory (e.g., a repository root). */
public interface CodeDirectory {

/** The filesystem directory path we are running against. */
Path asPath();

/**
* Find a file with the given trailing path. This is useful for situations in which you only know
* the last part of the path for a file within the project.
*/
Optional<Path> findFilesWithTrailingPath(final String path) throws IOException;

static CodeDirectory from(final Path projectDir) {
return new DefaultCodeDirectory(projectDir);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package io.codemodder;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

final class DefaultCodeDirectory implements CodeDirectory {

Expand All @@ -26,4 +33,31 @@ final class DefaultCodeDirectory implements CodeDirectory {
public Path asPath() {
return repositoryDir;
}

@Override
public Optional<Path> findFilesWithTrailingPath(final String path) throws IOException {
// find the files with the trailing path
AtomicReference<Path> found = new AtomicReference<>();

final String cleanPath =
path.trim()
.replace("\\\\", "\\")
.replace("//", "/")
.replace('\\', File.separatorChar)
.replace('/', File.separatorChar);

Files.walkFileTree(
repositoryDir,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
if (file.toString().endsWith(cleanPath)) {
found.set(file);
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
});
return Optional.ofNullable(found.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ private Optional<Map.Entry<String, RuleSarif>> tryToBuild(
final String toolName,
final String rule,
final SarifSchema210 sarif,
final Path repositoryRoot,
final CodeDirectory codeDirectory,
final List<RuleSarifFactory> factories) {
for (final var factory : factories) {
final var maybeRuleSarif = factory.build(toolName, rule, sarif, repositoryRoot);
final var maybeRuleSarif = factory.build(toolName, rule, sarif, codeDirectory);
if (maybeRuleSarif.isPresent()) {
return Optional.of(Map.entry(toolName, maybeRuleSarif.get()));
}
Expand Down Expand Up @@ -64,7 +64,7 @@ private String extractRuleId(final Result result, final Run run) {
}

private Stream<Map.Entry<String, RuleSarif>> fromSarif(
final Run run, final SarifSchema210 sarif, final Path repositoryRoot) {
final Run run, final SarifSchema210 sarif, final CodeDirectory codeDirectory) {
// driver name
final var toolName = run.getTool().getDriver().getName();
final List<RuleSarifFactory> factories =
Expand All @@ -81,20 +81,21 @@ private Stream<Map.Entry<String, RuleSarif>> fromSarif(
: Stream.<String>empty();

return allResults.flatMap(
rule -> tryToBuild(toolName, rule, sarif, repositoryRoot, factories).stream());
rule -> tryToBuild(toolName, rule, sarif, codeDirectory, factories).stream());
}

/**
* Parse a list of SARIF files and organize the obtained {@link RuleSarif}s by tool name with a
* map .
*/
@Override
public Map<String, List<RuleSarif>> parseIntoMap(
final List<Path> sarifFiles, final Path repositoryRoot) {
final List<Path> sarifFiles, final CodeDirectory codeDirectory) {
final var map = new HashMap<String, List<RuleSarif>>();
sarifFiles.stream()
.flatMap(f -> readSarifFile(f).stream())
.flatMap(
sarif -> sarif.getRuns().stream().flatMap(run -> fromSarif(run, sarif, repositoryRoot)))
sarif -> sarif.getRuns().stream().flatMap(run -> fromSarif(run, sarif, codeDirectory)))
.forEach(
p ->
map.merge(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.codemodder;

import com.contrastsecurity.sarif.SarifSchema210;
import java.nio.file.Path;
import java.util.Optional;

/** Builds {@link RuleSarif}s. */
public interface RuleSarifFactory {

/** Builds {@link RuleSarif}s if it supports {@code toolName}. */
Optional<RuleSarif> build(
String toolName, String rule, SarifSchema210 sarif, Path repositoryRoot);
String toolName, String rule, SarifSchema210 sarif, CodeDirectory codeDirectory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface SarifParser {
* Given a list of sarif {@link Path}s, organize them into a {@link Map} containing {@link
* RuleSarif}s organized by tool name.
*/
Map<String, List<RuleSarif>> parseIntoMap(List<Path> sarifFiles, Path repositoryRoot);
Map<String, List<RuleSarif>> parseIntoMap(List<Path> sarifFiles, CodeDirectory codeDirectory);

static SarifParser create() {
return new DefaultSarifParser();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.codemodder;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

final class DefaultCodeDirectoryTest {

private DefaultCodeDirectory codeDirectory;
private Path repoDir;

@BeforeEach
void setup(@TempDir final Path repoDir) throws IOException {
Path testFile1 = repoDir.resolve("my/other/test/file1.java");
Path srcFile1 = repoDir.resolve("src/main/file1.java");
Path srcFile2 = repoDir.resolve("src/main/file2.java");

Files.createDirectories(testFile1.getParent());
Files.createFile(testFile1);

Files.createDirectories(srcFile1.getParent());
Files.createFile(srcFile1);

Files.createDirectories(srcFile2.getParent());
Files.createFile(srcFile2);

Files.writeString(testFile1, "test file 1");
Files.writeString(srcFile1, "src file 1");
Files.writeString(srcFile2, "src file 2");

this.repoDir = repoDir;
codeDirectory = new DefaultCodeDirectory(repoDir);
}

@ParameterizedTest
@MethodSource("fileTests")
void it_finds_files(final String givenPath, final String expectedPath) throws IOException {
Optional<Path> filesWithTrailingPath = codeDirectory.findFilesWithTrailingPath(givenPath);

if (expectedPath == null) {
assertThat(filesWithTrailingPath).isEmpty();
} else {
assertThat(filesWithTrailingPath).isPresent();
Path expected = repoDir.resolve(expectedPath);
assertThat(filesWithTrailingPath.get()).isEqualTo(expected);
}
}

private static Stream<Arguments> fileTests() {
return Stream.of(
Arguments.of("file1.java", "my/other/test/file1.java"),
Arguments.of("main/file1.java", "src/main/file1.java"),
Arguments.of("main//file1.java", "src/main/file1.java"),
Arguments.of("main\\file1.java", "src/main/file1.java"),
Arguments.of("src\\\\main\\file1.java", "src/main/file1.java"),
Arguments.of("file2.java", "src/main/file2.java"),
Arguments.of("file3.java", null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,12 @@ private void verifyCodemod(
Path sonarJson = testResourceDir.resolve("sonar-issues.json");

// Check for any sarif files and build the RuleSarif map
CodeDirectory codeDir = CodeDirectory.from(tmpDir);
List<Path> allSarifs = new ArrayList<>();
Files.newDirectoryStream(testResourceDir, "*.sarif")
.iterator()
.forEachRemaining(allSarifs::add);
Map<String, List<RuleSarif>> map = SarifParser.create().parseIntoMap(allSarifs, tmpDir);
Map<String, List<RuleSarif>> map = SarifParser.create().parseIntoMap(allSarifs, codeDir);

// Check for any a defectdojo
Path defectDojo = testResourceDir.resolve("defectdojo.json");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ private void verifyCodemod(
files.filter(file -> file.getFileName().toString().endsWith(".sarif")).toList();
}

final CodeDirectory codeDirectory = CodeDirectory.from(tmpDir);
final Map<String, List<RuleSarif>> map =
SarifParser.create().parseIntoMap(allSarifFiles, tmpDir);
SarifParser.create().parseIntoMap(allSarifFiles, codeDirectory);

// grab all the .before and .after files in the dir
final List<Path> allBeforeFiles;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
package io.codemodder.providers.sarif.appscan;

import com.contrastsecurity.sarif.*;
import io.codemodder.CodeDirectory;
import io.codemodder.RuleSarif;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

/** A {@link RuleSarif} for AppScan results. */
final class AppScanRuleSarif implements RuleSarif {

private final SarifSchema210 sarif;
private final String ruleId;
private final Map<Path, List<Result>> resultsCache;
private final Path repositoryRoot;
private final List<String> locations;

/** A map of a AppScan SARIF "location" URIs mapped to their respective file paths. */
Expand All @@ -28,11 +23,10 @@ final class AppScanRuleSarif implements RuleSarif {
* Creates an {@link AppScanRuleSarif} that has already done the work of mapping AppScan SARIF
* locations, which are strange combinations of class name and file path, into predictable paths.
*/
public AppScanRuleSarif(
final String ruleId, final SarifSchema210 sarif, final Path repositoryRoot) {
AppScanRuleSarif(
final String ruleId, final SarifSchema210 sarif, final CodeDirectory codeDirectory) {
this.sarif = Objects.requireNonNull(sarif);
this.ruleId = Objects.requireNonNull(ruleId);
this.repositoryRoot = repositoryRoot;
this.resultsCache = new HashMap<>();
this.locations =
sarif.getRuns().get(0).getArtifacts().stream()
Expand All @@ -49,7 +43,7 @@ public AppScanRuleSarif(
// we have a real but partial path, now we have to find it in the repository
Optional<Path> existingRealPath;
try {
existingRealPath = findFileWithTrailingPath(path);
existingRealPath = codeDirectory.findFilesWithTrailingPath(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand All @@ -61,24 +55,6 @@ public AppScanRuleSarif(
this.artifactLocationIndices = Map.copyOf(artifactLocationIndicesMap);
}

private Optional<Path> findFileWithTrailingPath(final String path) throws IOException {
// find the files with the trailing path
AtomicReference<Path> found = new AtomicReference<>();
Files.walkFileTree(
repositoryRoot,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
if (file.toString().endsWith(path)) {
found.set(file);
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
});
return Optional.ofNullable(found.get());
}

@Override
public List<Region> getRegionsFromResultsByRule(final Path path) {
List<Result> resultsByLocationPath = getResultsByLocationPath(path);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package io.codemodder.providers.sarif.appscan;

import com.contrastsecurity.sarif.SarifSchema210;
import io.codemodder.CodeDirectory;
import io.codemodder.RuleSarif;
import io.codemodder.RuleSarifFactory;
import java.nio.file.Path;
import java.util.Optional;

/** A factory for building {@link AppScanRuleSarif}s. */
public final class AppScanRuleSarifFactory implements RuleSarifFactory {

@Override
public Optional<RuleSarif> build(
String toolName, String rule, SarifSchema210 sarif, Path repositoryRoot) {
final String toolName,
final String rule,
final SarifSchema210 sarif,
final CodeDirectory codeDirectory) {
if (AppScanRuleSarif.toolName.equals(toolName)) {
return Optional.of(new AppScanRuleSarif(rule, sarif, repositoryRoot));
return Optional.of(new AppScanRuleSarif(rule, sarif, codeDirectory));
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ void it_works_with_appscan_sarif(@TempDir final Path repoDir) throws IOException
new ObjectMapper().readValue(AppScanModuleTest.emptySarif, SarifSchema210.class);
AppScanRuleSarifFactory ruleSarifFactory = new AppScanRuleSarifFactory();
Optional<RuleSarif> ruleSarif =
ruleSarifFactory.build("HCL AppScan Static Analyzer", "SA2813462719", rawSarif, repoDir);
ruleSarifFactory.build(
"HCL AppScan Static Analyzer", "SA2813462719", rawSarif, CodeDirectory.from(repoDir));
assertThat(ruleSarif.isPresent(), is(true));
AppScanModule module =
new AppScanModule(List.of(AppScanSarifTestCodemod.class), List.of(ruleSarif.get()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.contrastsecurity.sarif.Result;
import com.contrastsecurity.sarif.SarifSchema210;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.codemodder.CodeDirectory;
import io.codemodder.RuleSarif;
import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -42,7 +43,7 @@ void it_parses_sarif_and_maps_java_locations(@TempDir final Path tmpDir) throws
new File("src/test/resources/webgoat_2023_8_binary.sarif"), SarifSchema210.class);
Optional<RuleSarif> sarifRef =
appScanRuleSarifFactory.build(
"HCL AppScan Static Analyzer", "SA2813462719", rawSarif, tmpDir);
"HCL AppScan Static Analyzer", "SA2813462719", rawSarif, CodeDirectory.from(tmpDir));
assertThat(sarifRef.isPresent()).isTrue();
RuleSarif ruleSarif = sarifRef.get();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.contrastsecurity.sarif.Result;
import com.contrastsecurity.sarif.Run;
import com.contrastsecurity.sarif.SarifSchema210;
import io.codemodder.CodeDirectory;
import io.codemodder.RuleSarif;
import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -25,10 +26,10 @@ public final class CodeQLRuleSarif implements RuleSarif {
private final Path repositoryRoot;

public CodeQLRuleSarif(
final String ruleId, final SarifSchema210 sarif, final Path repositoryRoot) {
final String ruleId, final SarifSchema210 sarif, final CodeDirectory codeDirectory) {
this.sarif = Objects.requireNonNull(sarif);
this.ruleId = Objects.requireNonNull(ruleId);
this.repositoryRoot = repositoryRoot;
this.repositoryRoot = codeDirectory.asPath();
this.resultsCache = new HashMap<>();
}

Expand Down
Loading

0 comments on commit 49e4d79

Please sign in to comment.