Skip to content

Commit 776ab26

Browse files
committed
Add -validation-server and -validation-server-auth params to validate detected endpoints against a running server
1 parent b77ba44 commit 776ab26

3 files changed

Lines changed: 227 additions & 10 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.denimgroup.threadfix.cli.endpoints;
2+
3+
import java.util.Map;
4+
5+
import static com.denimgroup.threadfix.CollectionUtils.map;
6+
7+
public class Credentials {
8+
public String authenticationEndpoint;
9+
public Map<String, String> parameters = map();
10+
11+
public Map<String, String> authenticatedParameters = null;
12+
}

src/com/denimgroup/threadfix/cli/endpoints/EndpointMain.java

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ enum Logging {
7979
static int totalDetectedParameters = 0;
8080

8181
static String testUrlPath = null;
82+
static Credentials testCredentials = null;
8283

8384
private static void println(String line) {
8485
if (printFormat != SIMPLE_JSON && printFormat != FULL_JSON) {
@@ -335,6 +336,25 @@ private static boolean checkArguments(String[] args) {
335336
} else if (arg.startsWith("-validation-server=")) {
336337
String[] parts = arg.split("=");
337338
testUrlPath = parts[1];
339+
} else if (arg.startsWith("-validation-server-auth=")) {
340+
arg = arg.substring("-validation-server-auth=".length());
341+
String[] parts = arg.split(";");
342+
testCredentials = new Credentials();
343+
testCredentials.parameters = map();
344+
345+
for (String part : parts) {
346+
if (testCredentials.authenticationEndpoint == null) {
347+
testCredentials.authenticationEndpoint = part;
348+
} else {
349+
String[] paramParts = part.split("=");
350+
if (paramParts.length != 2) {
351+
println("Invalid authentication parameter format: " + part);
352+
} else {
353+
testCredentials.parameters.put(paramParts[0], paramParts[1]);
354+
}
355+
}
356+
}
357+
338358
} else {
339359
println("Received unsupported option " + arg + ", valid arguments are -lint, -debug, -simple-json, -json, -path-list-file, and -simple");
340360
return false;
@@ -455,11 +475,22 @@ private static List<Endpoint> listEndpoints(File rootFile, Collection<FrameworkT
455475
println("Failed to validate serialization for at least one of these endpoints");
456476
}
457477

478+
// Run endpoint testing against a given server
458479
if (testUrlPath != null) {
459480
EndpointTester tester = new EndpointTester(testUrlPath);
460481

461482
println("Testing endpoints against server at: " + testUrlPath);
462483

484+
if (testCredentials != null) {
485+
try {
486+
if (tester.authorize(testCredentials, null) < 400) {
487+
println("Successfully authenticated");
488+
}
489+
} catch (IOException e) {
490+
println("Warning - unable to authorize against server");
491+
}
492+
}
493+
463494
List<Endpoint> successfulEndpoints = list();
464495
List<Endpoint> failedEndpoints = list();
465496
List<Endpoint> allEndpoints = EndpointUtil.flattenWithVariants(endpoints);
@@ -477,13 +508,19 @@ private static List<Endpoint> listEndpoints(File rootFile, Collection<FrameworkT
477508
}
478509

479510
try {
480-
tester.test(endpoint);
481-
successfulEndpoints.add(endpoint);
482-
} catch (FileNotFoundException | UnknownHostException e) {
483-
failedEndpoints.add(endpoint);
511+
int responseCode = tester.test(endpoint, testCredentials);
512+
if (responseCode != 404) {
513+
successfulEndpoints.add(endpoint);
514+
} else {
515+
failedEndpoints.add(endpoint);
516+
}
484517
} catch (IOException e) {
485-
if (e.getMessage().contains("HTTP")) {
518+
// Any non-404 error is considered "successful", since any other 4xx or 5xx may indicate
519+
// that the endpoint exists but incorrect parameters were provided
520+
if (e.getMessage().contains("Server returned HTTP response code") && !e.getMessage().contains("code: 404")) {
486521
successfulEndpoints.add(endpoint);
522+
} else {
523+
failedEndpoints.add(endpoint);
487524
}
488525
}
489526
}
@@ -493,6 +530,7 @@ private static List<Endpoint> listEndpoints(File rootFile, Collection<FrameworkT
493530
}
494531

495532
println(successfulEndpoints.size() + "/" + (successfulEndpoints.size() + failedEndpoints.size()) + " endpoints were queryable");
533+
println("(" + (allEndpoints.size() - successfulEndpoints.size() - failedEndpoints.size()) + " endpoints skipped since they had a wildcard in the URL)");
496534
}
497535

498536
int numMissingStartLine = 0;
Lines changed: 172 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package com.denimgroup.threadfix.cli.endpoints;
22

3+
import com.denimgroup.threadfix.data.entities.RouteParameter;
4+
import com.denimgroup.threadfix.data.entities.RouteParameterType;
35
import com.denimgroup.threadfix.data.interfaces.Endpoint;
46
import com.denimgroup.threadfix.framework.util.PathUtil;
57

68
import java.io.IOException;
7-
import java.net.HttpURLConnection;
8-
import java.net.MalformedURLException;
9-
import java.net.URL;
10-
import java.net.URLConnection;
9+
import java.io.OutputStream;
10+
import java.io.UnsupportedEncodingException;
11+
import java.net.*;
12+
import java.nio.charset.StandardCharsets;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.StringJoiner;
16+
17+
import static com.denimgroup.threadfix.CollectionUtils.list;
18+
import static com.denimgroup.threadfix.CollectionUtils.map;
1119

1220
public class EndpointTester {
1321
String basePath;
@@ -16,12 +24,171 @@ public EndpointTester(String basePath) {
1624
this.basePath = basePath;
1725
}
1826

19-
public int test(Endpoint endpoint) throws IOException {
27+
public int test(Endpoint endpoint, Credentials credentials) throws IOException {
2028
URL url = new URL(PathUtil.combine(this.basePath, endpoint.getUrlPath()));
2129
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
2230
conn.setRequestMethod(endpoint.getHttpMethod());
2331

32+
if (credentials != null && credentials.authenticatedParameters != null) {
33+
for (Map.Entry<String, String> header : credentials.authenticatedParameters.entrySet()) {
34+
conn.setRequestProperty(header.getKey(), header.getValue());
35+
}
36+
}
37+
2438
conn.getInputStream().close();
2539
return conn.getResponseCode();
2640
}
41+
42+
public int authorize(Credentials credentials, Endpoint endpoint) throws IOException {
43+
// Get query settings
44+
String httpMethod = endpoint != null ? endpoint.getHttpMethod() : "POST";
45+
46+
HttpURLConnection.setFollowRedirects(false);
47+
48+
// Try auth by best-match
49+
50+
if (endpoint != null) {
51+
try {
52+
String urlParams = configureRequestWithBestMatchParameters(endpoint.getParameters(), credentials, null);
53+
String finalizedPath = credentials.authenticationEndpoint;
54+
if (!urlParams.isEmpty()) {
55+
finalizedPath += "?" + urlParams;
56+
}
57+
58+
URL url = new URL(PathUtil.combine(this.basePath, finalizedPath));
59+
// Configure connection
60+
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
61+
conn.setRequestMethod(httpMethod);
62+
configureRequestWithBestMatchParameters(endpoint.getParameters(), credentials, conn);
63+
64+
conn.getInputStream().close();
65+
if (conn.getResponseCode() < 400 && saveCredentialsResponse(conn, credentials)) {
66+
return conn.getResponseCode();
67+
}
68+
} catch (IOException e) {
69+
System.out.println("Unable to authorize using best-match parameters:");
70+
e.printStackTrace();
71+
}
72+
}
73+
74+
try {
75+
URL url = new URL(PathUtil.combine(this.basePath, credentials.authenticationEndpoint));
76+
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
77+
conn.setRequestMethod(httpMethod);
78+
configureRequestWithFormParameters(credentials, conn);
79+
80+
conn.getInputStream().close();
81+
if (conn.getResponseCode() < 400 && saveCredentialsResponse(conn, credentials)) {
82+
return conn.getResponseCode();
83+
}
84+
} catch (IOException e) {
85+
System.out.println("Unable to authorize using all-forms parameters:");
86+
e.printStackTrace();
87+
}
88+
89+
return -1;
90+
}
91+
92+
private boolean saveCredentialsResponse(HttpURLConnection conn, Credentials creds) {
93+
if (conn.getHeaderField("Set-Cookie") != null) {
94+
List<String> cookies = conn.getHeaderFields().get("Set-Cookie");
95+
List<String> sanitizeCookies = list();
96+
97+
for (String cookie : cookies) {
98+
String mainPart = cookie;
99+
if (mainPart.contains(";")) {
100+
mainPart = mainPart.substring(0, mainPart.indexOf(';'));
101+
}
102+
sanitizeCookies.add(mainPart);
103+
}
104+
105+
creds.authenticatedParameters = map();
106+
creds.authenticatedParameters.put("Cookie", String.join("; ", sanitizeCookies));
107+
108+
return true;
109+
}
110+
return false;
111+
}
112+
113+
private String configureRequestWithBestMatchParameters(Map<String, RouteParameter> parsedParameters, Credentials credentials, HttpURLConnection conn) throws IOException {
114+
StringJoiner queryString = new StringJoiner("&");
115+
StringJoiner formParams = new StringJoiner("&");
116+
117+
for (Map.Entry<String, String> credParam : credentials.parameters.entrySet()) {
118+
RouteParameter paramSpec = null;
119+
if (parsedParameters != null) {
120+
if (parsedParameters.containsKey(credParam.getKey())) {
121+
paramSpec = parsedParameters.get(credParam.getKey());
122+
}
123+
}
124+
125+
if (paramSpec == null) {
126+
paramSpec = RouteParameter.fromDataType(credParam.getKey(), "String");
127+
paramSpec.setParamType(RouteParameterType.FORM_DATA);
128+
}
129+
130+
String encodedKey, encodedValue;
131+
try {
132+
encodedKey = URLEncoder.encode(credParam.getKey(), "UTF-8");
133+
encodedValue = URLEncoder.encode(credParam.getValue(), "UTF-8");
134+
} catch (UnsupportedEncodingException e) {
135+
e.printStackTrace();
136+
continue;
137+
}
138+
139+
String encodedPair = encodedKey + "=" + encodedValue;
140+
141+
switch (paramSpec.getParamType()) {
142+
case QUERY_STRING:
143+
queryString.add(encodedPair);
144+
break;
145+
146+
default:
147+
// Assume form data for anything else
148+
formParams.add(encodedPair);
149+
}
150+
}
151+
152+
if (formParams.length() > 0 && conn != null) {
153+
154+
byte[] out = formParams.toString().getBytes(StandardCharsets.UTF_8);
155+
156+
conn.setDoOutput(true);
157+
conn.setFixedLengthStreamingMode(out.length);
158+
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
159+
160+
conn.connect();
161+
try (OutputStream os = conn.getOutputStream()) {
162+
os.write(out);
163+
}
164+
}
165+
166+
if (queryString.length() == 0) {
167+
return "";
168+
} else {
169+
return "?" + queryString.toString();
170+
}
171+
}
172+
173+
private void configureRequestWithFormParameters(Credentials credentials, HttpURLConnection conn) throws IOException {
174+
StringJoiner joiner = new StringJoiner("&");
175+
for (Map.Entry<String, String> param : credentials.parameters.entrySet()) {
176+
try {
177+
joiner.add(URLEncoder.encode(param.getKey(), "UTF-8") + "=" + URLEncoder.encode(param.getValue(), "UTF-8"));
178+
} catch (UnsupportedEncodingException e) {
179+
e.printStackTrace();
180+
}
181+
}
182+
183+
byte[] out = joiner.toString().getBytes(StandardCharsets.UTF_8);
184+
185+
conn.setDoOutput(true);
186+
conn.setFixedLengthStreamingMode(out.length);
187+
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
188+
189+
conn.connect();
190+
try (OutputStream os = conn.getOutputStream()) {
191+
os.write(out);
192+
}
193+
}
27194
}

0 commit comments

Comments
 (0)