Skip to content

Commit 4b58536

Browse files
authored
Remediator API and adjacent classes refactoring (#455)
Adds a new API for the remediator/searcher pattern we use for codemods. The aim is to eliminate a lot of duplicate/similar-ish code around the remediators. Also converts the Sonar JNDI codemod to use the new API. Classes from the previous API are still there, just renamed with the `Legacy` prefix.
1 parent d444088 commit 4b58536

File tree

43 files changed

+902
-465
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+902
-465
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import io.codemodder.providers.sonar.RuleIssue;
88
import io.codemodder.providers.sonar.SonarRemediatingJavaParserChanger;
99
import io.codemodder.remediation.GenericRemediationMetadata;
10+
import io.codemodder.remediation.Remediator;
1011
import io.codemodder.remediation.jndiinjection.JNDIInjectionRemediator;
1112
import io.codemodder.sonar.model.Issue;
1213
import io.codemodder.sonar.model.SonarFinding;
1314
import java.util.List;
1415
import java.util.Objects;
16+
import java.util.Optional;
1517
import javax.inject.Inject;
1618

1719
/** This codemod knows how to fix JNDI vulnerabilities found by sonar. */
@@ -22,15 +24,15 @@
2224
importance = Importance.HIGH)
2325
public final class SonarJNDIInjectionCodemod extends SonarRemediatingJavaParserChanger {
2426

25-
private final JNDIInjectionRemediator remediator;
2627
private final RuleIssue issues;
28+
private final Remediator<Issue> remediator;
2729

2830
@Inject
2931
public SonarJNDIInjectionCodemod(
3032
@ProvidedSonarScan(ruleId = "javasecurity:S2078") final RuleIssue issues) {
3133
super(GenericRemediationMetadata.JNDI.reporter(), issues);
3234
this.issues = Objects.requireNonNull(issues);
33-
this.remediator = JNDIInjectionRemediator.DEFAULT;
35+
this.remediator = new JNDIInjectionRemediator<>();
3436
}
3537

3638
@Override
@@ -52,7 +54,13 @@ public CodemodFileScanningResult visit(
5254
issuesForFile,
5355
SonarFinding::getKey,
5456
i -> i.getTextRange() != null ? i.getTextRange().getStartLine() : i.getLine(),
55-
i -> i.getTextRange() != null ? i.getTextRange().getEndLine() : null,
56-
i -> i.getTextRange() != null ? i.getTextRange().getStartOffset() : null);
57+
i ->
58+
i.getTextRange() != null
59+
? Optional.of(i.getTextRange().getEndLine())
60+
: Optional.empty(),
61+
i ->
62+
i.getTextRange() != null
63+
? Optional.of(i.getTextRange().getStartOffset() + 1)
64+
: Optional.empty());
5765
}
5866
}

core-codemods/src/main/java/io/codemodder/codemods/remediators/ssrf/DefaultSSRFRemediator.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
import io.codemodder.codetf.DetectorRule;
1616
import io.codemodder.codetf.FixedFinding;
1717
import io.codemodder.codetf.UnfixedFinding;
18-
import io.codemodder.remediation.FixCandidate;
19-
import io.codemodder.remediation.FixCandidateSearchResults;
20-
import io.codemodder.remediation.FixCandidateSearcher;
18+
import io.codemodder.remediation.LegacyFixCandidate;
19+
import io.codemodder.remediation.LegacyFixCandidateSearchResults;
20+
import io.codemodder.remediation.LegacyFixCandidateSearcher;
2121
import io.codemodder.remediation.MethodOrConstructor;
2222
import io.github.pixee.security.HostValidator;
2323
import io.github.pixee.security.Urls;
@@ -42,17 +42,17 @@ public <T> CodemodFileScanningResult remediateAll(
4242

4343
// search for new URL() calls on those lines, assuming the tool points there -- there are plenty
4444
// of more signatures to chase down
45-
FixCandidateSearcher<T> searcher =
46-
new FixCandidateSearcher.Builder<T>()
45+
LegacyFixCandidateSearcher<T> searcher =
46+
new LegacyFixCandidateSearcher.Builder<T>()
4747
.withMatcher(mce -> mce.isConstructorForType("URL"))
4848
.withMatcher(mce -> !mce.getArguments().isEmpty())
4949
.build();
5050

5151
// Matches calls like RestTemplate.exchange(...)
5252
// Doesn't actually check that the `exchange` call scope is actually of type RestTemplate
5353
// This is left for the detectors, for now
54-
FixCandidateSearcher<T> rtSearcher =
55-
new FixCandidateSearcher.Builder<T>()
54+
LegacyFixCandidateSearcher<T> rtSearcher =
55+
new LegacyFixCandidateSearcher.Builder<T>()
5656
// is method with name
5757
.withMatcher(mce -> mce.isMethodCallWithName("exchange"))
5858
// has a scope
@@ -153,7 +153,7 @@ private boolean hardenRT(final CompilationUnit cu, final MethodCallExpr call) {
153153
* issues, and a fixer predicate, that returns true if the change is successful.
154154
*/
155155
private <T> Pair<List<CodemodChange>, List<UnfixedFinding>> searchAndFix(
156-
final FixCandidateSearcher<T> searcher,
156+
final LegacyFixCandidateSearcher<T> searcher,
157157
final BiPredicate<CompilationUnit, MethodOrConstructor> fixer,
158158
final CompilationUnit cu,
159159
final String path,
@@ -166,7 +166,7 @@ private <T> Pair<List<CodemodChange>, List<UnfixedFinding>> searchAndFix(
166166
List<CodemodChange> changes = new ArrayList<>();
167167
List<UnfixedFinding> unfixedFindings = new ArrayList<>();
168168

169-
FixCandidateSearchResults<T> results =
169+
LegacyFixCandidateSearchResults<T> results =
170170
searcher.search(
171171
cu,
172172
path,
@@ -177,7 +177,7 @@ private <T> Pair<List<CodemodChange>, List<UnfixedFinding>> searchAndFix(
177177
getEndLine,
178178
getStartColumn);
179179

180-
for (FixCandidate<T> candidate : results.fixCandidates()) {
180+
for (LegacyFixCandidate<T> candidate : results.fixCandidates()) {
181181
MethodOrConstructor call = candidate.call();
182182
List<T> issues = candidate.issues();
183183
if (fixer.test(cu, call)) {

core-codemods/src/main/java/io/codemodder/codemods/remediators/weakrandom/DefaultWeakRandomRemediator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public <T> CodemodFileScanningResult remediateAll(
5050
detectorRule,
5151
path,
5252
getLine.apply(issue),
53-
RemediationMessages.multipleCallsFound));
53+
RemediationMessages.multipleNodesFound));
5454
continue;
5555
} else if (unsafeRandoms.isEmpty()) {
5656
unfixedFindings.add(
@@ -59,7 +59,7 @@ public <T> CodemodFileScanningResult remediateAll(
5959
detectorRule,
6060
path,
6161
getLine.apply(issue),
62-
RemediationMessages.noCallsAtThatLocation));
62+
RemediationMessages.noNodesAtThatLocation));
6363
continue;
6464
}
6565

framework/codemodder-base/src/main/java/io/codemodder/remediation/DefaultFixCandidateSearcher.java

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@
33
import com.github.javaparser.Position;
44
import com.github.javaparser.ast.CompilationUnit;
55
import com.github.javaparser.ast.Node;
6-
import com.github.javaparser.ast.expr.MethodCallExpr;
7-
import com.github.javaparser.ast.expr.ObjectCreationExpr;
86
import io.codemodder.codetf.DetectorRule;
97
import io.codemodder.codetf.UnfixedFinding;
108
import java.util.*;
119
import java.util.function.Function;
1210
import java.util.function.Predicate;
1311
import org.jetbrains.annotations.VisibleForTesting;
1412

13+
/**
14+
* Maps issues of type T to relevant nodes in the AST. Relevant nodes are decided with matchers.
15+
*
16+
* @param <T>
17+
*/
1518
final class DefaultFixCandidateSearcher<T> implements FixCandidateSearcher<T> {
1619

17-
private final List<Predicate<MethodOrConstructor>> matchers;
18-
private final String methodName;
20+
private final List<Predicate<Node>> matchers;
1921

20-
DefaultFixCandidateSearcher(
21-
final String methodName, final List<Predicate<MethodOrConstructor>> matchers) {
22-
this.methodName = methodName;
22+
DefaultFixCandidateSearcher(final List<Predicate<Node>> matchers) {
2323
this.matchers = matchers;
2424
}
2525

@@ -31,61 +31,58 @@ public FixCandidateSearchResults<T> search(
3131
final List<T> issuesForFile,
3232
final Function<T, String> getKey,
3333
final Function<T, Integer> getStartLine,
34-
final Function<T, Integer> getEndLine,
35-
final Function<T, Integer> getColumn) {
34+
final Function<T, Optional<Integer>> getEndLine,
35+
final Function<T, Optional<Integer>> getColumn) {
3636

3737
List<UnfixedFinding> unfixedFindings = new ArrayList<>();
3838

39-
List<MethodOrConstructor> calls =
39+
List<Node> nodes =
4040
cu.findAll(Node.class).stream()
41-
// limit to just the interesting nodes for us
42-
.filter(n -> n instanceof MethodCallExpr || n instanceof ObjectCreationExpr)
43-
// turn them into our convenience type
44-
.map(MethodOrConstructor::new)
45-
// don't find calls we may have added -- you can pick these out by them not having a
46-
// range yet
47-
.filter(MethodOrConstructor::hasRange)
48-
// filter by method name if one is provided in the search
49-
.filter(mce -> methodName == null || mce.isMethodCallWithName(methodName))
5041
// filter by matchers
51-
.filter(mce -> matchers.stream().allMatch(m -> m.test(mce)))
42+
.filter(n -> matchers.stream().allMatch(m -> m.test(n)))
5243
.toList();
5344

54-
Map<MethodOrConstructor, List<T>> fixCandidateToIssueMapping = new HashMap<>();
45+
Map<Node, List<T>> fixCandidateToIssueMapping = new HashMap<>();
5546

5647
for (T issue : issuesForFile) {
5748
String findingId = getKey.apply(issue);
5849
int issueStartLine = getStartLine.apply(issue);
59-
Integer issueEndLine = getEndLine.apply(issue);
60-
Integer column = getColumn.apply(issue);
61-
List<MethodOrConstructor> callsForIssue =
62-
calls.stream()
63-
.filter(MethodOrConstructor::hasRange)
50+
Optional<Integer> maybeEndLine = getEndLine.apply(issue);
51+
Optional<Integer> maybeColumn = getColumn.apply(issue);
52+
List<Node> nodesForIssue =
53+
nodes.stream()
6454
.filter(
65-
mce -> {
66-
int callStartLine = mce.getRange().begin.line;
67-
return matches(issueStartLine, issueEndLine, callStartLine);
55+
n -> {
56+
int nodeStartLine = n.getRange().orElseThrow().begin.line;
57+
// if issue end line info is present, match the node line with the issue range
58+
return maybeEndLine
59+
.map(issueEndLine -> matches(issueStartLine, nodeStartLine, issueEndLine))
60+
.orElse(matches(issueStartLine, nodeStartLine));
6861
})
62+
// if column info is present, check if the column is contained in the node's range
63+
.filter(
64+
n ->
65+
maybeColumn
66+
.map(
67+
column ->
68+
n.getRange()
69+
.orElseThrow()
70+
.contains(new Position(issueStartLine, column)))
71+
.orElse(true))
6972
.toList();
70-
if (callsForIssue.size() > 1 && column != null) {
71-
callsForIssue =
72-
callsForIssue.stream()
73-
.filter(mce -> mce.getRange().contains(new Position(issueStartLine, column)))
74-
.toList();
75-
}
76-
if (callsForIssue.isEmpty()) {
73+
if (nodesForIssue.isEmpty()) {
7774
unfixedFindings.add(
7875
new UnfixedFinding(
79-
findingId, rule, path, issueStartLine, RemediationMessages.noCallsAtThatLocation));
76+
findingId, rule, path, issueStartLine, RemediationMessages.noNodesAtThatLocation));
8077
continue;
81-
} else if (callsForIssue.size() > 1) {
78+
} else if (nodesForIssue.size() > 1) {
8279
unfixedFindings.add(
8380
new UnfixedFinding(
84-
findingId, rule, path, issueStartLine, RemediationMessages.multipleCallsFound));
81+
findingId, rule, path, issueStartLine, RemediationMessages.multipleNodesFound));
8582
continue;
8683
}
87-
MethodOrConstructor call = callsForIssue.get(0);
88-
fixCandidateToIssueMapping.computeIfAbsent(call, k -> new ArrayList<>()).add(issue);
84+
Node node = nodesForIssue.get(0);
85+
fixCandidateToIssueMapping.computeIfAbsent(node, k -> new ArrayList<>()).add(issue);
8986
}
9087

9188
List<FixCandidate<T>> fixCandidates =
@@ -108,13 +105,15 @@ public List<FixCandidate<T>> fixCandidates() {
108105

109106
@VisibleForTesting
110107
static boolean matches(
111-
final int issueStartLine, final Integer issueEndLine, final int startCallLine) {
112-
// if the issue spans multiple lines, the call must be within that range
113-
if (issueEndLine != null) {
114-
return isInBetween(startCallLine, issueStartLine, issueEndLine);
115-
}
116-
// if the issue is on a single line, the call must be on that line
117-
return startCallLine == issueStartLine;
108+
final int issueStartLine, final int startNodeLine, final int issueEndLine) {
109+
// if the issue spans multiple lines, the node must be within that range
110+
return matches(issueStartLine, startNodeLine)
111+
&& isInBetween(startNodeLine, issueStartLine, issueEndLine);
112+
}
113+
114+
static boolean matches(final int issueStartLine, final int startNodeLine) {
115+
// if the issue is on a single line, the node must be on that line
116+
return startNodeLine == issueStartLine;
118117
}
119118

120119
private static boolean isInBetween(int number, int lowerBound, int upperBound) {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package io.codemodder.remediation;
2+
3+
import com.github.javaparser.Position;
4+
import com.github.javaparser.ast.CompilationUnit;
5+
import com.github.javaparser.ast.Node;
6+
import com.github.javaparser.ast.expr.MethodCallExpr;
7+
import com.github.javaparser.ast.expr.ObjectCreationExpr;
8+
import io.codemodder.codetf.DetectorRule;
9+
import io.codemodder.codetf.UnfixedFinding;
10+
import java.util.*;
11+
import java.util.function.Function;
12+
import java.util.function.Predicate;
13+
import org.jetbrains.annotations.VisibleForTesting;
14+
15+
@Deprecated
16+
final class DefaultLegacyFixCandidateSearcher<T> implements LegacyFixCandidateSearcher<T> {
17+
18+
private final List<Predicate<MethodOrConstructor>> matchers;
19+
private final String methodName;
20+
21+
DefaultLegacyFixCandidateSearcher(
22+
final String methodName, final List<Predicate<MethodOrConstructor>> matchers) {
23+
this.methodName = methodName;
24+
this.matchers = matchers;
25+
}
26+
27+
@Override
28+
public LegacyFixCandidateSearchResults<T> search(
29+
final CompilationUnit cu,
30+
final String path,
31+
final DetectorRule rule,
32+
final List<T> issuesForFile,
33+
final Function<T, String> getKey,
34+
final Function<T, Integer> getStartLine,
35+
final Function<T, Integer> getEndLine,
36+
final Function<T, Integer> getColumn) {
37+
38+
List<UnfixedFinding> unfixedFindings = new ArrayList<>();
39+
40+
List<MethodOrConstructor> calls =
41+
cu.findAll(Node.class).stream()
42+
// limit to just the interesting nodes for us
43+
.filter(n -> n instanceof MethodCallExpr || n instanceof ObjectCreationExpr)
44+
// turn them into our convenience type
45+
.map(MethodOrConstructor::new)
46+
// don't find calls we may have added -- you can pick these out by them not having a
47+
// range yet
48+
.filter(MethodOrConstructor::hasRange)
49+
// filter by method name if one is provided in the search
50+
.filter(mce -> methodName == null || mce.isMethodCallWithName(methodName))
51+
// filter by matchers
52+
.filter(mce -> matchers.stream().allMatch(m -> m.test(mce)))
53+
.toList();
54+
55+
Map<MethodOrConstructor, List<T>> fixCandidateToIssueMapping = new HashMap<>();
56+
57+
for (T issue : issuesForFile) {
58+
String findingId = getKey.apply(issue);
59+
int issueStartLine = getStartLine.apply(issue);
60+
Integer issueEndLine = getEndLine.apply(issue);
61+
Integer column = getColumn.apply(issue);
62+
List<MethodOrConstructor> callsForIssue =
63+
calls.stream()
64+
.filter(MethodOrConstructor::hasRange)
65+
.filter(
66+
mce -> {
67+
int callStartLine = mce.getRange().begin.line;
68+
return matches(issueStartLine, issueEndLine, callStartLine);
69+
})
70+
.toList();
71+
if (callsForIssue.size() > 1 && column != null) {
72+
callsForIssue =
73+
callsForIssue.stream()
74+
.filter(mce -> mce.getRange().contains(new Position(issueStartLine, column)))
75+
.toList();
76+
}
77+
if (callsForIssue.isEmpty()) {
78+
unfixedFindings.add(
79+
new UnfixedFinding(
80+
findingId, rule, path, issueStartLine, RemediationMessages.noNodesAtThatLocation));
81+
continue;
82+
} else if (callsForIssue.size() > 1) {
83+
unfixedFindings.add(
84+
new UnfixedFinding(
85+
findingId, rule, path, issueStartLine, RemediationMessages.multipleNodesFound));
86+
continue;
87+
}
88+
MethodOrConstructor call = callsForIssue.get(0);
89+
fixCandidateToIssueMapping.computeIfAbsent(call, k -> new ArrayList<>()).add(issue);
90+
}
91+
92+
List<LegacyFixCandidate<T>> legacyFixCandidates =
93+
fixCandidateToIssueMapping.entrySet().stream()
94+
.map(entry -> new LegacyFixCandidate<>(entry.getKey(), entry.getValue()))
95+
.toList();
96+
97+
return new LegacyFixCandidateSearchResults<T>() {
98+
@Override
99+
public List<UnfixedFinding> unfixableFindings() {
100+
return unfixedFindings;
101+
}
102+
103+
@Override
104+
public List<LegacyFixCandidate<T>> fixCandidates() {
105+
return legacyFixCandidates;
106+
}
107+
};
108+
}
109+
110+
@VisibleForTesting
111+
static boolean matches(
112+
final int issueStartLine, final Integer issueEndLine, final int startCallLine) {
113+
// if the issue spans multiple lines, the call must be within that range
114+
if (issueEndLine != null) {
115+
return isInBetween(startCallLine, issueStartLine, issueEndLine);
116+
}
117+
// if the issue is on a single line, the call must be on that line
118+
return startCallLine == issueStartLine;
119+
}
120+
121+
private static boolean isInBetween(int number, int lowerBound, int upperBound) {
122+
return number >= lowerBound && number <= upperBound;
123+
}
124+
}

0 commit comments

Comments
 (0)