Skip to content

Commit 182513a

Browse files
authored
Merge pull request github#13235 from atorralba/atorralba/java/hudson-models
Java: Add Hudson models
2 parents 36e8441 + ad2f558 commit 182513a

File tree

13 files changed

+201
-39
lines changed

13 files changed

+201
-39
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added more models for the Hudson framework.

java/ql/lib/ext/hudson.model.model.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ extensions:
1616
data:
1717
- ["hudson.model", "Node", True, "createPath", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
1818
- ["hudson.model", "DirectoryBrowserSupport$Path", False, "Path", "(String,String,boolean,long,boolean,long)", "", "Argument[0]", "Argument[this].SyntheticField[hudson.model.DirectoryBrowserSupport$Path.href]", "taint", "ai-manual"]
19+
- addsTo:
20+
pack: codeql/java-all
21+
extensible: sourceModel
22+
data:
23+
- ["hudson.model", "Descriptor", True, "configure", "", "", "Parameter", "remote", "manual"]
24+
- ["hudson.model", "Descriptor", True, "newInstance", "", "", "Parameter", "remote", "manual"]

java/ql/lib/ext/hudson.model.yml

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,68 @@ extensions:
33
pack: codeql/java-all
44
extensible: sinkModel
55
data:
6-
- ["hudson", "FilePath", False, "copyFrom", "(FilePath)", "", "Argument[0]", "path-injection", "manual"]
7-
- ["hudson", "FilePath", False, "copyFrom", "(URL)", "", "Argument[0]", "path-injection", "manual"]
8-
- ["hudson", "FilePath", False, "copyFrom", "(FileItem)", "", "Argument[0]", "path-injection", "ai-manual"]
9-
- ["hudson", "FilePath", False, "copyRecursiveTo", "(DirScanner,FilePath,String,TarCompression)", "", "Argument[1]", "path-injection", "ai-manual"]
10-
- ["hudson", "FilePath", False, "copyRecursiveTo", "(DirScanner,FilePath,String)", "", "Argument[1]", "file-content-store", "ai-manual"]
11-
- ["hudson", "FilePath", False, "copyRecursiveTo", "(String,FilePath)", "", "Argument[1]", "path-injection", "ai-manual"]
12-
- ["hudson", "FilePath", False, "copyRecursiveTo", "(String,String,FilePath)", "", "Argument[0]", "path-injection", "ai-manual"]
13-
- ["hudson", "FilePath", False, "copyRecursiveTo", "(String,String,FilePath)", "", "Argument[2]", "path-injection", "ai-manual"]
14-
- ["hudson", "FilePath", False, "copyTo", "(FilePath)", "", "Argument[0]", "path-injection", "ai-manual"]
15-
- ["hudson", "FilePath", False, "installIfNecessaryFrom", "(URL,TaskListener,String)", "", "Argument[0]", "request-forgery", "ai-manual"]
16-
- ["hudson", "FilePath", False, "newInputStreamDenyingSymlinkAsNeeded", "(File,String,boolean)", "", "Argument[0]", "path-injection", "ai-manual"]
6+
- ["hudson", "FilePath", True, "copyFrom", "", "", "Argument[this]", "path-injection", "manual"]
7+
- ["hudson", "FilePath", True, "copyFrom", "(FilePath)", "", "Argument[0]", "path-injection", "manual"]
8+
- ["hudson", "FilePath", True, "copyFrom", "(URL)", "", "Argument[0]", "path-injection", "manual"]
9+
- ["hudson", "FilePath", True, "copyFrom", "(FileItem)", "", "Argument[0]", "path-injection", "ai-manual"]
10+
- ["hudson", "FilePath", True, "copyRecursiveTo", "", "", "Argument[this]", "path-injection", "ai-manual"]
11+
- ["hudson", "FilePath", True, "copyRecursiveTo", "(DirScanner,FilePath,String,TarCompression)", "", "Argument[1]", "path-injection", "ai-manual"]
12+
- ["hudson", "FilePath", True, "copyRecursiveTo", "(DirScanner,FilePath,String)", "", "Argument[1]", "file-content-store", "ai-manual"]
13+
- ["hudson", "FilePath", True, "copyRecursiveTo", "(String,FilePath)", "", "Argument[1]", "path-injection", "ai-manual"]
14+
- ["hudson", "FilePath", True, "copyRecursiveTo", "(String,String,FilePath)", "", "Argument[0]", "path-injection", "ai-manual"]
15+
- ["hudson", "FilePath", True, "copyRecursiveTo", "(String,String,FilePath)", "", "Argument[2]", "path-injection", "ai-manual"]
16+
- ["hudson", "FilePath", True, "copyTo", "", "", "Argument[this]", "path-injection", "manual"]
17+
- ["hudson", "FilePath", True, "copyTo", "(FilePath)", "", "Argument[0]", "path-injection", "ai-manual"]
18+
- ["hudson", "FilePath", True, "copyToWithPermission", "", "", "Argument[this]", "path-injection", "manual"]
19+
- ["hudson", "FilePath", True, "copyToWithPermission", "(FilePath)", "", "Argument[0]", "path-injection", "manual"]
20+
- ["hudson", "FilePath", True, "installIfNecessaryFrom", "(URL,TaskListener,String)", "", "Argument[0]", "request-forgery", "ai-manual"]
21+
- ["hudson", "FilePath", True, "newInputStreamDenyingSymlinkAsNeeded", "(File,String,boolean)", "", "Argument[0]", "path-injection", "ai-manual"]
22+
- ["hudson", "FilePath", True, "openInputStream", "(File,OpenOption[])", "", "Argument[0]", "path-injection", "manual"]
23+
- ["hudson", "FilePath", True, "read", "", "", "Argument[this]", "path-injection", "manual"]
24+
- ["hudson", "FilePath", True, "read", "(FilePath,OpenOption[])", "", "Argument[0]", "path-injection", "manual"]
25+
- ["hudson", "FilePath", True, "readFromOffset", "", "", "Argument[this]", "path-injection", "manual"]
26+
- ["hudson", "FilePath", True, "readToString", "", "", "Argument[this]", "path-injection", "manual"]
27+
- ["hudson", "FilePath", True, "renameTo", "", "", "Argument[this]", "path-injection", "manual"]
28+
- ["hudson", "FilePath", True, "renameTo", "", "", "Argument[0]", "path-injection", "manual"]
29+
- ["hudson", "FilePath", True, "write", "", "", "Argument[this]", "path-injection", "manual"]
30+
- ["hudson", "FilePath", True, "write", "(String,String)", "", "Argument[0]", "file-content-store", "manual"]
31+
- ["hudson", "Launcher$ProcStarter", False, "cmds", "", "", "Argument[0]", "command-injection", "manual"]
32+
- ["hudson", "Launcher$ProcStarter", False, "cmdAsSingleString", "", "", "Argument[0]", "command-injection", "manual"]
33+
- ["hudson", "Launcher", True, "launch", "", "", "Argument[0]", "command-injection", "manual"]
34+
- ["hudson", "Launcher", True, "launchChannel", "", "", "Argument[0]", "command-injection", "manual"]
35+
- addsTo:
36+
pack: codeql/java-all
37+
extensible: sourceModel
38+
data:
39+
- ["hudson", "Plugin", True, "configure", "", "", "Parameter", "remote", "manual"]
40+
- ["hudson", "Plugin", True, "newInstance", "", "", "Parameter", "remote", "manual"]
1741
- addsTo:
1842
pack: codeql/java-all
1943
extensible: summaryModel
2044
data:
21-
- ["hudson", "FilePath", False, "child", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
22-
- ["hudson", "FilePath", False, "list", "(String,String,boolean)", "", "Argument[this]", "ReturnValue", "taint", "ai-manual"]
23-
- ["hudson", "FilePath", False, "list", "(String,String)", "", "Argument[this]", "ReturnValue", "taint", "ai-manual"]
24-
- ["hudson", "FilePath", False, "list", "(String)", "", "Argument[this]", "ReturnValue", "taint", "ai-manual"]
25-
- ["hudson", "FilePath", False, "normalize", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
26-
- ["hudson", "FilePath", False, "sibling", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
45+
- ["hudson", "FilePath", True, "FilePath", "(String)", "", "Argument[0]", "Argument[this]", "taint", "manual"]
46+
- ["hudson", "FilePath", True, "FilePath", "(FilePath,String)", "", "Argument[0..1]", "Argument[this]", "taint", "manual"]
47+
- ["hudson", "FilePath", True, "FilePath", "(VirtualChannel,String)", "", "Argument[1]", "Argument[this]", "taint", "manual"]
48+
- ["hudson", "FilePath", True, "child", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
49+
- ["hudson", "FilePath", True, "list", "(String,String,boolean)", "", "Argument[this]", "ReturnValue", "taint", "ai-manual"]
50+
- ["hudson", "FilePath", True, "list", "(String,String)", "", "Argument[this]", "ReturnValue", "taint", "ai-manual"]
51+
- ["hudson", "FilePath", True, "list", "(String)", "", "Argument[this]", "ReturnValue", "taint", "ai-manual"]
52+
- ["hudson", "FilePath", True, "normalize", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
53+
- ["hudson", "FilePath", True, "sibling", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
54+
- ["hudson", "Util", True, "nullify", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
55+
- ["hudson", "Util", True, "fixNull", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
56+
- ["hudson", "Util", True, "fixEmpty", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
57+
- ["hudson", "Util", True, "fixEmptyAndTrim", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
58+
- ["hudson", "Util", True, "getFileName", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
59+
- ["hudson", "Util", True, "join", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
60+
- ["hudson", "Util", True, "encodeRFC2396", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
61+
- ["hudson", "Util", True, "wrapToErrorSpan", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
62+
- ["hudson", "Util", True, "fileToPath", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
63+
- ["hudson", "Util", True, "xmlEscape", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
64+
- ["hudson", "Util", True, "escape", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
65+
- ["hudson", "Util", True, "singleQuote", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
66+
- ["hudson", "Util", True, "rawEncode", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
67+
- ["hudson", "Util", True, "encode", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
68+
- ["hudson", "Util", True, "fromHexString", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
69+
- ["hudson", "Util", True, "toHexString", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]
70+
- ["hudson", "Util", True, "tokenize", "", "", "Argument[0]", "ReturnValue", "taint", "manual"]

java/ql/lib/ext/hudson.util.model.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ extensions:
77
- ["hudson.util", "AtomicFileWriter", True, "AtomicFileWriter", "(Path,Charset,boolean,boolean)", "", "Argument[0]", "path-injection", "ai-manual"]
88
- ["hudson.util", "AtomicFileWriter", True, "AtomicFileWriter", "(Path,Charset)", "", "Argument[0]", "path-injection", "ai-manual"]
99
- ["hudson.util", "ClasspathBuilder", True, "add", "(FilePath)", "", "Argument[0]", "path-injection", "ai-manual"]
10+
- ["hudson.util", "FormValidation", True, "errorWithMarkup", "", "", "Argument[0]", "html-injection", "manual"]
11+
- ["hudson.util", "FormValidation", True, "okWithMarkup", "", "", "Argument[0]", "html-injection", "manual"]
12+
- ["hudson.util", "FormValidation", True, "respond", "", "", "Argument[1]", "html-injection", "manual"]
13+
- ["hudson.util", "FormValidation", True, "warningWithMarkup", "", "", "Argument[0]", "html-injection", "manual"]
1014
- ["hudson.util", "IOUtils", True, "mkdirs", "(File)", "", "Argument[0]", "path-injection", "ai-manual"]
1115
- ["hudson.util", "StreamTaskListener", True, "StreamTaskListener", "(File,boolean,Charset)", "", "Argument[0]", "path-injection", "ai-manual"]
1216
- ["hudson.util", "TextFile", True, "delete", "()", "", "Argument[this]", "path-injection", "manual"]
@@ -15,10 +19,28 @@ extensions:
1519
- ["hudson.util", "TextFile", True, "lines", "()", "", "Argument[this]", "path-injection", "manual"]
1620
- ["hudson.util", "TextFile", True, "read", "()", "", "Argument[this]", "path-injection", "manual"]
1721
- ["hudson.util", "TextFile", True, "readTrim", "()", "", "Argument[this]", "path-injection", "manual"]
22+
- ["hudson.util", "TextFile", True, "write", "(String)", "", "Argument[this]", "path-injection", "manual"]
1823
- ["hudson.util", "TextFile", True, "write", "(String)", "", "Argument[0]", "file-content-store", "manual"]
24+
- ["hudson.util", "HttpResponses", True, "staticResource", "(File)", "", "Argument[0]", "path-injection", "manual"]
1925
- addsTo:
2026
pack: codeql/java-all
2127
extensible: summaryModel
2228
data:
29+
- ["hudson.util", "ArgumentListBuilder", True, "ArgumentListBuilder", "", "", "Argument[0]", "Argument[this]", "taint", "manual"]
30+
- ["hudson.util", "ArgumentListBuilder", True, "add", "", "", "Argument[0]", "Argument[this]", "taint", "manual"]
31+
- ["hudson.util", "ArgumentListBuilder", True, "clone", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
32+
- ["hudson.util", "ArgumentListBuilder", True, "prepend", "", "", "Argument[0]", "Argument[this]", "taint", "manual"]
33+
- ["hudson.util", "ArgumentListBuilder", True, "toCommandArray", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
34+
- ["hudson.util", "ArgumentListBuilder", True, "toList", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
35+
- ["hudson.util", "ArgumentListBuilder", True, "toWindowsCommand", "", "", "Argument[this]", "ReturnValue", "taint", "manual"]
36+
# ArgumentListBuilder fluent methods
37+
- ["hudson.util", "ArgumentListBuilder", True, "add", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
38+
- ["hudson.util", "ArgumentListBuilder", True, "addKeyValuePair", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
39+
- ["hudson.util", "ArgumentListBuilder", True, "addKeyValuePairs", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
40+
- ["hudson.util", "ArgumentListBuilder", True, "addKeyValuePairsFromPropertyString", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
41+
- ["hudson.util", "ArgumentListBuilder", True, "addMasked", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
42+
- ["hudson.util", "ArgumentListBuilder", True, "addQuoted", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
43+
- ["hudson.util", "ArgumentListBuilder", True, "addTokenized", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
44+
- ["hudson.util", "ArgumentListBuilder", True, "prepend", "", "", "Argument[this]", "ReturnValue", "value", "manual"]
2345
- ["hudson.util", "QuotedStringTokenizer", True, "tokenize", "(String)", "", "Argument[0]", "ReturnValue", "taint", "ai-manual"]
2446
- ["hudson.util", "TextFile", True, "TextFile", "(File)", "", "Argument[0]", "Argument[this]", "taint", "ai-manual"]

java/ql/lib/semmle/code/java/dataflow/FlowSources.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ abstract class RemoteFlowSource extends DataFlow::Node {
3636
abstract string getSourceType();
3737
}
3838

39+
/**
40+
* A module for importing frameworks that define flow sources.
41+
*/
42+
private module FlowSources {
43+
private import semmle.code.java.frameworks.hudson.Hudson
44+
}
45+
3946
private class ExternalRemoteFlowSource extends RemoteFlowSource {
4047
ExternalRemoteFlowSource() { sourceNode(this, "remote") }
4148

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/** Provides classes and predicates related to the Hudson framework. */
2+
3+
import java
4+
private import semmle.code.java.dataflow.FlowSources
5+
private import semmle.code.java.security.XSS
6+
7+
private class FilePathRead extends LocalUserInput {
8+
FilePathRead() {
9+
this.asExpr()
10+
.(MethodAccess)
11+
.getMethod()
12+
.hasQualifiedName("hudson", "FilePath",
13+
[
14+
"newInputStreamDenyingSymlinkAsNeeded", "openInputStream", "read", "readFromOffset",
15+
"readToString"
16+
])
17+
}
18+
}
19+
20+
private class HudsonUtilXssSanitizer extends XssSanitizer {
21+
HudsonUtilXssSanitizer() {
22+
this.asExpr()
23+
.(MethodAccess)
24+
.getMethod()
25+
// Not including xmlEscape because it only accounts for >, <, and &.
26+
// It does not account for ", or ', which makes it an incomplete XSS sanitizer.
27+
.hasQualifiedName("hudson", "Util", "escape")
28+
}
29+
}

java/ql/lib/semmle/code/java/security/XSS.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import semmle.code.java.frameworks.android.WebView
66
import semmle.code.java.frameworks.spring.SpringController
77
import semmle.code.java.frameworks.spring.SpringHttp
88
import semmle.code.java.frameworks.javaee.jsf.JSFRenderer
9+
private import semmle.code.java.frameworks.hudson.Hudson
910
import semmle.code.java.dataflow.DataFlow
1011
import semmle.code.java.dataflow.TaintTracking
1112
private import semmle.code.java.dataflow.ExternalFlow
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import hudson.FilePath;
2+
3+
public class Hudson {
4+
5+
private static void sink(Object o) {}
6+
7+
public static void test() throws Exception {
8+
FilePath fp = null;
9+
sink(FilePath.newInputStreamDenyingSymlinkAsNeeded(null, null, null)); // $hasLocalValueFlow
10+
sink(FilePath.openInputStream(null, null)); // $hasLocalValueFlow
11+
sink(fp.read()); // $hasLocalValueFlow
12+
sink(fp.read(null)); // $hasLocalValueFlow
13+
sink(fp.readFromOffset(-1)); // $hasLocalValueFlow
14+
sink(fp.readToString()); // $hasLocalValueFlow
15+
}
16+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/google-android-9.0.0:${testdir}/../../../stubs/playframework-2.6.x:${testdir}/../../../stubs/jackson-databind-2.12:${testdir}/../../../stubs/jackson-core-2.12:${testdir}/../../../stubs/akka-2.6.x:${testdir}/../../../stubs/jwtk-jjwt-0.11.2
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/google-android-9.0.0:${testdir}/../../../stubs/playframework-2.6.x:${testdir}/../../../stubs/jackson-databind-2.12:${testdir}/../../../stubs/jackson-core-2.12:${testdir}/../../../stubs/akka-2.6.x:${testdir}/../../../stubs/jwtk-jjwt-0.11.2:${testdir}/../../../stubs/jenkins

java/ql/test/query-tests/security/CWE-079/semmle/tests/XSS.java

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,19 @@
44

55
package test.cwe079.cwe.examples;
66

7-
8-
9-
107
import java.io.IOException;
118
import javax.servlet.ServletException;
129
import javax.servlet.http.Cookie;
1310
import javax.servlet.http.HttpServlet;
1411
import javax.servlet.http.HttpServletRequest;
1512
import javax.servlet.http.HttpServletResponse;
1613

17-
1814
public class XSS extends HttpServlet {
1915
protected void doGet(HttpServletRequest request, HttpServletResponse response)
20-
throws ServletException, IOException {
16+
throws ServletException, IOException {
2117
// BAD: a request parameter is written directly to the Servlet response stream
22-
response.getWriter().print(
23-
"The page \"" + request.getParameter("page") + "\" was not found."); // $xss
18+
response.getWriter()
19+
.print("The page \"" + request.getParameter("page") + "\" was not found."); // $xss
2420

2521
// GOOD: servlet API encodes the error message HTML for the HTML context
2622
response.sendError(HttpServletResponse.SC_NOT_FOUND,
@@ -29,35 +25,31 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
2925
// GOOD: escape HTML characters first
3026
response.sendError(HttpServletResponse.SC_NOT_FOUND,
3127
"The page \"" + encodeForHtml(request.getParameter("page")) + "\" was not found.");
32-
28+
3329
// GOOD: servlet API encodes the error message HTML for the HTML context
3430
response.sendError(HttpServletResponse.SC_NOT_FOUND,
3531
"The page \"" + capitalizeName(request.getParameter("page")) + "\" was not found.");
36-
32+
3733
// BAD: outputting the path of the resource
3834
response.getWriter().print("The path section of the URL was " + request.getPathInfo()); // $xss
3935

40-
// BAD: typical XSS, this time written to an OutputStream instead of a Writer
36+
// BAD: typical XSS, this time written to an OutputStream instead of a Writer
4137
response.getOutputStream().write(request.getPathInfo().getBytes()); // $xss
42-
}
43-
44-
45-
46-
47-
4838

39+
// GOOD: sanitizer
40+
response.getOutputStream().write(hudson.Util.escape(request.getPathInfo()).getBytes()); // safe
41+
}
4942

5043
/**
51-
* Replace special characters in the given text such that it can
52-
* be inserted into an HTML file and not be interpreted as including
53-
* any HTML tags.
44+
* Replace special characters in the given text such that it can be inserted into an HTML file
45+
* and not be interpreted as including any HTML tags.
5446
*/
5547
static String encodeForHtml(String text) {
5648
// This is just a stub. For an example of a real implementation, see
5749
// the OWASP Java Encoder Project.
5850
return text.replace("<", "&lt;");
5951
}
60-
52+
6153
static String capitalizeName(String text) {
6254
return text.replace("foo inc", "Foo, Inc.");
6355
}

0 commit comments

Comments
 (0)