Skip to content

Commit 3c36768

Browse files
committed
add sarif parsing basics
1 parent 02bd5c5 commit 3c36768

File tree

15 files changed

+345
-31
lines changed

15 files changed

+345
-31
lines changed

core-codemods/src/main/java/io/codemodder/codemods/SQLParameterizer.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
import java.util.stream.Collectors;
3030
import org.javatuples.Pair;
3131

32-
/** Contains most of the logic for the SQLParameterizerCodemod. */
33-
final class SQLParameterizer {
32+
/**
33+
* Contains most of the logic for detecting and fixing parameterizable SQL statements for a given
34+
* {@link MethodCallExpr}.
35+
*/
36+
public final class SQLParameterizer {
3437

3538
private static final String preparedStatementNamePrefix = "stmt";
3639
private static final String preparedStatementNamePrefixAlternative = "statement";
@@ -39,7 +42,7 @@ final class SQLParameterizer {
3942

4043
private CompilationUnit compilationUnit;
4144

42-
SQLParameterizer(final MethodCallExpr methodCallExpr) {
45+
public SQLParameterizer(final MethodCallExpr methodCallExpr) {
4346
this.executeCall = Objects.requireNonNull(methodCallExpr);
4447
this.compilationUnit = null;
4548
}
@@ -500,7 +503,7 @@ private void fix(
500503
* Checks if {@code methodCall} is a query call that needs to be fixed and fixes if that's the
501504
* case.
502505
*/
503-
boolean checkAndFix() {
506+
public boolean checkAndFix() {
504507

505508
if (executeCall.findCompilationUnit().isPresent()) {
506509
this.compilationUnit = executeCall.findCompilationUnit().get();

framework/codemodder-base/src/main/java/io/codemodder/LazyLoadingRuleSarif.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ private void checkInitialized() {
3838
}
3939

4040
@Override
41-
public List<Result> getResultsByPath(final Path path) {
41+
public List<Result> getResultsByLocationPath(final Path path) {
4242
checkInitialized();
43-
return ruleSarif.getResultsByPath(path);
43+
return ruleSarif.getResultsByLocationPath(path);
4444
}
4545

4646
@Override

framework/codemodder-base/src/main/java/io/codemodder/RuleSarif.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ public interface RuleSarif {
2121
List<Region> getRegionsFromResultsByRule(Path path);
2222

2323
/**
24-
* Get all the SARIF results with the matching path
24+
* Get all the SARIF results with the matching path for the first location field
2525
*
2626
* @param path the file being scanned
2727
* @return the results associated with the given file
2828
*/
29-
List<Result> getResultsByPath(Path path);
29+
List<Result> getResultsByLocationPath(Path path);
3030

3131
/** Return the entire SARIF as a model in case more comprehensive inspection is needed. */
3232
SarifSchema210 rawDocument();
@@ -46,7 +46,7 @@ public List<Region> getRegionsFromResultsByRule(final Path path) {
4646
}
4747

4848
@Override
49-
public List<Result> getResultsByPath(final Path path) {
49+
public List<Result> getResultsByLocationPath(final Path path) {
5050
return List.of();
5151
}
5252

framework/codemodder-base/src/main/java/io/codemodder/SarifPluginJavaParserChanger.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ protected SarifPluginJavaParserChanger(
9191

9292
public List<CodemodChange> visit(
9393
final CodemodInvocationContext context, final CompilationUnit cu) {
94-
List<Result> results = sarif.getResultsByPath(context.path());
94+
List<Result> results = sarif.getResultsByLocationPath(context.path());
9595

9696
// small shortcut to avoid always executing the expensive findAll
9797
if (results.isEmpty()) {

framework/codemodder-base/src/main/java/io/codemodder/SarifPluginRawFileChanger.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ protected SarifPluginRawFileChanger(
2020

2121
@Override
2222
public List<CodemodChange> visitFile(final CodemodInvocationContext context) {
23-
List<Result> results = sarif.getResultsByPath(context.path());
23+
List<Result> results = sarif.getResultsByLocationPath(context.path());
2424
if (!results.isEmpty()) {
2525
return onFileFound(context, results);
2626
}

gradle/build-plugins/src/main/kotlin/io.codemodder.maven-publish.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extensions.getByType<nebula.plugin.contacts.ContactsExtension>().run {
2222

2323
val publicationName = "nebula"
2424
signing {
25+
setRequired(false)
2526
if (providers.environmentVariable("CI").isPresent) {
2627
val signingKey: String? by project
2728
val signingPassword: String? by project

plugins/codemodder-plugin-appscan/src/main/java/io/codemodder/providers/sarif/appscan/AppScanModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
import java.util.stream.Collectors;
1212
import java.util.stream.Stream;
1313

14-
/** Responsible for distributing the SARIFS to AppSCan based codemods based on rules. */
14+
/** Responsible for distributing the SARIFS to AppScan based codemods based on rules. */
1515
public final class AppScanModule extends AbstractModule {
1616

1717
private final List<Class<? extends CodeChanger>> codemodTypes;
1818
private final List<RuleSarif> allAppScanRuleSarifs;
1919

20-
AppScanModule(
20+
public AppScanModule(
2121
final List<Class<? extends CodeChanger>> codemodTypes, final List<RuleSarif> sarifs) {
2222
this.codemodTypes = Objects.requireNonNull(codemodTypes);
2323
this.allAppScanRuleSarifs = sarifs;

plugins/codemodder-plugin-appscan/src/main/java/io/codemodder/providers/sarif/appscan/AppScanRuleSarif.java

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package io.codemodder.providers.sarif.appscan;
22

3-
import com.contrastsecurity.sarif.Region;
4-
import com.contrastsecurity.sarif.Result;
5-
import com.contrastsecurity.sarif.SarifSchema210;
3+
import com.contrastsecurity.sarif.*;
64
import io.codemodder.RuleSarif;
5+
import java.io.IOException;
6+
import java.io.UncheckedIOException;
7+
import java.nio.file.FileVisitResult;
8+
import java.nio.file.Files;
79
import java.nio.file.Path;
8-
import java.util.HashMap;
9-
import java.util.List;
10-
import java.util.Map;
11-
import java.util.Objects;
10+
import java.nio.file.SimpleFileVisitor;
11+
import java.nio.file.attribute.BasicFileAttributes;
12+
import java.util.*;
13+
import java.util.concurrent.atomic.AtomicReference;
1214

1315
/** A {@link RuleSarif} for AppScan results. */
1416
final class AppScanRuleSarif implements RuleSarif {
@@ -17,23 +19,104 @@ final class AppScanRuleSarif implements RuleSarif {
1719
private final String ruleId;
1820
private final Map<Path, List<Result>> resultsCache;
1921
private final Path repositoryRoot;
22+
private final List<String> locations;
2023

24+
/** A map of a HCL SARIF "location" URIs mapped to their respective file paths. */
25+
private final Map<Path, Set<Integer>> artifactLocationIndices;
26+
27+
/**
28+
* Creates an {@link AppScanRuleSarif} that has already done the work of mapping HCL SARIF
29+
* locations, which are strange combinations of class name and file path, into predictable paths.
30+
*/
2131
public AppScanRuleSarif(
2232
final String ruleId, final SarifSchema210 sarif, final Path repositoryRoot) {
2333
this.sarif = Objects.requireNonNull(sarif);
2434
this.ruleId = Objects.requireNonNull(ruleId);
2535
this.repositoryRoot = repositoryRoot;
2636
this.resultsCache = new HashMap<>();
37+
this.locations =
38+
sarif.getRuns().get(0).getArtifacts().stream()
39+
.map(Artifact::getLocation)
40+
.map(ArtifactLocation::getUri)
41+
.map(u -> u.substring(8))
42+
.toList();
43+
Map<Path, Set<Integer>> artifactLocationIndices = new HashMap<>();
44+
45+
for (int i = 0; i < locations.size(); i++) {
46+
final Integer index = i;
47+
String path = locations.get(i);
48+
path = path.replace('\\', '/');
49+
// we have a real but partial path, now we have to find it in the repository
50+
Optional<Path> existingRealPath;
51+
try {
52+
existingRealPath = findFileWithTrailingPath(path);
53+
} catch (IOException e) {
54+
throw new UncheckedIOException(e);
55+
}
56+
57+
// add to the map if we found a matching file
58+
existingRealPath.ifPresent(
59+
p ->
60+
artifactLocationIndices
61+
.computeIfAbsent(repositoryRoot.relativize(p), k -> new HashSet<>())
62+
.add(index));
63+
}
64+
this.artifactLocationIndices = Map.copyOf(artifactLocationIndices);
65+
}
66+
67+
private Optional<Path> findFileWithTrailingPath(final String path) throws IOException {
68+
// find the files with the trailing path
69+
AtomicReference<Path> found = new AtomicReference<>();
70+
Files.walkFileTree(
71+
repositoryRoot,
72+
new SimpleFileVisitor<>() {
73+
@Override
74+
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
75+
if (file.toString().endsWith(path)) {
76+
found.set(file);
77+
return FileVisitResult.TERMINATE;
78+
}
79+
return FileVisitResult.CONTINUE;
80+
}
81+
});
82+
return Optional.ofNullable(found.get());
2783
}
2884

2985
@Override
3086
public List<Region> getRegionsFromResultsByRule(final Path path) {
31-
return List.of();
87+
List<Result> resultsByLocationPath = getResultsByLocationPath(path);
88+
return resultsByLocationPath.stream()
89+
.map(result -> result.getLocations().get(0).getPhysicalLocation().getRegion())
90+
.toList();
3291
}
3392

93+
/**
94+
* This call receives an actual source file path, whereas the HCL results store a reference to a
95+
* fully qualified class name plus ".java", e.g.:
96+
*
97+
* <pre>file:///org/owasp/webgoat/lessons/challenges/challenge5/Assignment5.java</pre>
98+
*/
3499
@Override
35-
public List<Result> getResultsByPath(final Path path) {
36-
return List.of();
100+
public List<Result> getResultsByLocationPath(final Path path) {
101+
return resultsCache.computeIfAbsent(
102+
path,
103+
p ->
104+
sarif.getRuns().stream()
105+
.flatMap(run -> run.getResults().stream())
106+
.filter(result -> result.getRuleId().equals(ruleId))
107+
.filter(
108+
result ->
109+
artifactLocationIndices.get(path) != null
110+
&& artifactLocationIndices
111+
.get(path)
112+
.contains(
113+
result
114+
.getLocations()
115+
.get(0)
116+
.getPhysicalLocation()
117+
.getArtifactLocation()
118+
.getIndex()))
119+
.toList());
37120
}
38121

39122
@Override
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.codemodder.providers.sarif.appscan;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.is;
5+
import static org.hamcrest.Matchers.notNullValue;
6+
7+
import com.contrastsecurity.sarif.Region;
8+
import com.google.inject.Guice;
9+
import com.google.inject.Injector;
10+
import io.codemodder.*;
11+
import io.codemodder.codetf.CodeTFReference;
12+
import java.io.IOException;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.nio.file.StandardOpenOption;
16+
import java.util.List;
17+
import javax.inject.Inject;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.io.TempDir;
21+
22+
final class AppScanModuleTest {
23+
24+
private Path repoDir;
25+
26+
@Codemod(
27+
id = "appscan-test:java/finds-stuff",
28+
importance = Importance.LOW,
29+
reviewGuidance = ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW)
30+
static class AppScanSarifTestCodemod implements CodeChanger {
31+
private final RuleSarif ruleSarif;
32+
33+
@Inject
34+
AppScanSarifTestCodemod(@ProvidedAppScanScan(ruleId = "SA2813462719") RuleSarif ruleSarif) {
35+
this.ruleSarif = ruleSarif;
36+
}
37+
38+
@Override
39+
public String getSummary() {
40+
return null;
41+
}
42+
43+
@Override
44+
public String getDescription() {
45+
return null;
46+
}
47+
48+
@Override
49+
public List<CodeTFReference> getReferences() {
50+
return null;
51+
}
52+
53+
@Override
54+
public String getIndividualChangeDescription(Path filePath, CodemodChange change) {
55+
return null;
56+
}
57+
}
58+
59+
@BeforeEach
60+
void setup(@TempDir final Path tmpDir) {
61+
AppScanRuleSarifFactory factory = new AppScanRuleSarifFactory();
62+
factory.build("appscan", "SA2813462719", null, null);
63+
this.repoDir = tmpDir;
64+
}
65+
66+
@Test
67+
void it_works_with_appscan_sarif() throws IOException {
68+
String javaCode = "class Foo { \n Object a = new Thing(); \n }";
69+
70+
Path javaFile = Files.createTempFile(repoDir, "HasThing", ".java");
71+
Files.writeString(javaFile, javaCode, StandardOpenOption.TRUNCATE_EXISTING);
72+
AppScanModule module = createModule(List.of(AppScanSarifTestCodemod.class));
73+
Injector injector = Guice.createInjector(module);
74+
AppScanSarifTestCodemod instance = injector.getInstance(AppScanSarifTestCodemod.class);
75+
76+
RuleSarif ruleSarif = instance.ruleSarif;
77+
assertThat(ruleSarif, is(notNullValue()));
78+
List<Region> regions = ruleSarif.getRegionsFromResultsByRule(javaFile);
79+
assertThat(regions.size(), is(1));
80+
}
81+
82+
private AppScanModule createModule(final List<Class<? extends CodeChanger>> codemodTypes) {
83+
return new AppScanModule(codemodTypes, List.of());
84+
}
85+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.codemodder.providers.sarif.appscan;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.contrastsecurity.sarif.Region;
6+
import com.contrastsecurity.sarif.Result;
7+
import com.contrastsecurity.sarif.SarifSchema210;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import io.codemodder.RuleSarif;
10+
import java.io.File;
11+
import java.io.IOException;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.util.List;
15+
import java.util.Optional;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.io.TempDir;
18+
19+
final class AppScanRuleSarifFactoryTest {
20+
21+
/**
22+
* In this test we'll attempt to load the SARIF file and build a {@link RuleSarif} from it, adn
23+
* confirm we can get the right locations for result guid 9833fe60-ded1-ee11-9f02-14cb65725114.
24+
*/
25+
@Test
26+
void it_parses_sarif_and_maps_java_locations(@TempDir final Path tmpDir) throws IOException {
27+
28+
// create the webgoat file in the repository dir
29+
String expectedPath =
30+
"src/main/java/org/owasp/webgoat/lessons/challenges/challenge5/Assignment5.java";
31+
Path actualAssignmentedJavaFilePath = tmpDir.resolve(expectedPath);
32+
actualAssignmentedJavaFilePath.toFile().getParentFile().mkdirs();
33+
34+
// create the file contents that this SARIF has results for
35+
Files.copy(Path.of("src/test/resources/Assignment5.java.txt"), actualAssignmentedJavaFilePath);
36+
37+
// read the SARIF file and build a RuleSarif from it
38+
AppScanRuleSarifFactory appScanRuleSarifFactory = new AppScanRuleSarifFactory();
39+
SarifSchema210 rawSarif =
40+
new ObjectMapper()
41+
.readValue(
42+
new File("src/test/resources/webgoat_2023_8_binary.sarif"), SarifSchema210.class);
43+
Optional<RuleSarif> sarifRef =
44+
appScanRuleSarifFactory.build(
45+
"HCL AppScan Static Analyzer", "SA2813462719", rawSarif, tmpDir);
46+
assertThat(sarifRef.isPresent()).isTrue();
47+
RuleSarif ruleSarif = sarifRef.get();
48+
49+
// verify the rule sarif has the right stuff
50+
assertThat(ruleSarif.getRule()).isEqualTo("SA2813462719");
51+
assertThat(ruleSarif.getDriver()).isEqualTo("HCL AppScan Static Analyzer");
52+
assertThat(ruleSarif.rawDocument()).isEqualTo(rawSarif);
53+
54+
// get the results for the file path (not the weird HCL thing) and confirm we have the right
55+
// results
56+
Path p = Path.of(expectedPath);
57+
List<Result> resultsForPath = ruleSarif.getResultsByLocationPath(p);
58+
assertThat(resultsForPath).isNotEmpty();
59+
60+
// get the regions affected by the given file
61+
List<Region> regions = ruleSarif.getRegionsFromResultsByRule(p);
62+
63+
// there is an injection in two form parameters in the SQL, so these 2 share the same sink
64+
assertThat(regions).hasSize(2);
65+
assertThat(regions.get(0).getStartLine()).isEqualTo(59);
66+
assertThat(regions.get(1).getStartLine()).isEqualTo(59);
67+
}
68+
}

0 commit comments

Comments
 (0)