Skip to content

Commit 02595a9

Browse files
feat: utilize the OpenAI API for tunneling detection
1 parent d88ba4b commit 02595a9

10 files changed

Lines changed: 270 additions & 35 deletions

File tree

.github/workflows/gradle.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ jobs:
1818

1919
runs-on: ubuntu-latest
2020

21+
env:
22+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
23+
2124
steps:
2225
- uses: actions/checkout@v3
2326
- name: Set up JDK

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies {
3636
implementation("org.atteo:evo-inflector:1.3")
3737
implementation("org.apache.opennlp:opennlp-tools:2.3.0")
3838
implementation("nz.ac.waikato.cms.weka:weka-stable:3.8.6")
39+
implementation("com.openai:openai-java:1.6.1")
3940
}
4041

4142
shadowJar {

src/main/java/cli/RestRulerCli.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public class RestRulerCli implements Runnable {
2828
description = "Specify the naming convention to use (camelcase or kebabcase). Default is kebabcase.")
2929
private String namingConvention;
3030

31+
@Option(names = {"-llm", "--enableExternalLLM"},
32+
description = "Enable the usage of LLMs for the evaluation of some rules.")
33+
private boolean enableLLM;
34+
3135
public static void main(String[] args) {
3236
PicocliRunner.run(RestRulerCli.class, args);
3337
}
@@ -46,7 +50,7 @@ public void run() {
4650
} else {
4751
output.setNamingConventionRules(false);
4852
}
49-
53+
output.setEnableLLM(this.enableLLM);
5054
if (filename != null)
5155
output.startAnalysis(this.openApiPath, this.filename);
5256
else

src/main/java/cli/ai/gpt.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cli.ai;
2+
3+
import cli.analyzer.RestAnalyzer;
4+
import cli.rule.IRestRule;
5+
import cli.rule.Violation;
6+
import cli.rule.constants.*;
7+
import com.openai.client.OpenAIClient;
8+
import com.openai.client.okhttp.OpenAIOkHttpClient;
9+
import com.openai.models.ChatModel;
10+
import com.openai.models.responses.Response;
11+
import com.openai.models.responses.ResponseCreateParams;
12+
13+
import java.util.List;
14+
15+
// gpt is a class that brings together the LLM-related functionality in the CLI
16+
// different rules may use the methods in this class to establish a connection to an external LLM agent for rule evaluation
17+
public class gpt {
18+
private OpenAIClient client;
19+
20+
private final String modelNoResponse = "no";
21+
22+
private final String systemMessageTunneling = """
23+
You are a REST API design validator. Your task is to determine whether HTTP method tunneling is present in a given endpoint.
24+
25+
An endpoint is considered to be tunneling if it uses a certain HTTP method to perform actions that are more appropriately represented by another HTTP method.
26+
27+
HTTP Method Definitions (use these exact criteria):
28+
- POST must be used to create a new resource in a collection or to execute controllers and not for other purposes
29+
- GET must be used to retrieve a representation of a resource and not for other purposes
30+
- DELETE should be used to delete a resource and not for other purposes
31+
- PUT must be used to both insert and update a stored resource and not for other purposes
32+
33+
If the actual HTTP method does not align with the conventional RESTful method implied by the endpoint's summary or description, classify it as tunneling.
34+
35+
Only respond with a one-word valid method name (one of POST, GET, DELETE or PUT) or "No" if no violation is detected.
36+
Do not explain your reasoning.
37+
38+
Examples:
39+
40+
Method: POST
41+
Description: Update a list.
42+
Summary: Update a list based on input.
43+
Response: PUT
44+
45+
Method: PUT
46+
Description: Submit a new application for review.
47+
Summary: Create a new application
48+
Response: POST
49+
50+
Method: DELETE
51+
Description: Deactivate a user account.
52+
Summary: Soft delete a user
53+
Response: No
54+
""";
55+
56+
public gpt() {
57+
this.client = OpenAIOkHttpClient.fromEnv();
58+
}
59+
60+
public void tunnelingViolationAI(IRestRule rule, String keyPath, String description, String summary,
61+
String requestType, String requestTypeMessage, String improvement, List<Violation> violations) {
62+
String input = this.constructTunnelingInput(description, summary, requestType);
63+
ResponseCreateParams params = ResponseCreateParams.builder()
64+
.temperature(0)
65+
.instructions(this.systemMessageTunneling)
66+
.input(input)
67+
.model(ChatModel.GPT_4_1_MINI)
68+
.build();
69+
Response response = this.client.responses().create(params);
70+
71+
response.output().stream()
72+
.flatMap(item -> item.message().stream())
73+
.flatMap(message -> message.content().stream())
74+
.flatMap(content -> content.outputText().stream())
75+
.forEach(outputText -> {
76+
String output = outputText.text();
77+
78+
if (!output.toLowerCase().equals(this.modelNoResponse) && !output.toUpperCase().equals(requestType.toUpperCase())) {
79+
violations.add(new Violation(rule, RestAnalyzer.locMapper.getLOCOfPath(keyPath),
80+
requestTypeMessage + improvement + output,
81+
keyPath,
82+
ErrorMessage.REQUESTTYPE));
83+
}
84+
});
85+
}
86+
87+
private String constructTunnelingInput(String description, String summary, String requestType) {
88+
String res = String.format("Method: %s", requestType);
89+
90+
if (description != null && !description.isEmpty()) {
91+
res += String.format("\nDescription: %s", description);
92+
}
93+
94+
if (summary != null && !summary.isEmpty()) {
95+
res += String.format("\nSummary: %s", summary);
96+
}
97+
98+
return res;
99+
}
100+
}

src/main/java/cli/rule/IRestRule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public interface IRestRule {
2323
boolean getIsActive();
2424

2525
void setIsActive(boolean isActive);
26+
27+
/**
28+
* Method used to determine whether the rule will use an LLM-based approach during evaluation.
29+
* This method is only used by rules that actually have an LLM component, so not every rule needs to implement a body for this.
30+
*
31+
* @param enableLLM determines whether LLM usage is enabled or not
32+
*/
33+
default void setEnableLLM(boolean enableLLM) {}
2634

2735
/**
2836
* Method used to check for any violations of the implemented rule

src/main/java/cli/rule/rules/RequestTypeDescriptionRule.java

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.swagger.v3.oas.models.Paths;
99
import org.apache.commons.lang3.tuple.ImmutablePair;
1010
import cli.weka.RequestMethodsWekaClassifier;
11+
import cli.ai.gpt;
1112

1213
import java.util.ArrayList;
1314
import java.util.List;
@@ -18,13 +19,17 @@ public class RequestTypeDescriptionRule implements IRestRule {
1819
private static final RuleIdentifier RULE_IDENTIFIER = RuleIdentifier.REQUEST_TYPE_DESCRIPTION;
1920
static final RuleCategory RULE_CATEGORY = RuleCategory.META;
2021
static final RuleSeverity RULE_SEVERITY = RuleSeverity.WARNING;
21-
static final List<RuleSoftwareQualityAttribute> SOFTWARE_QUALITY_ATTRIBUTES = List.of(RuleSoftwareQualityAttribute.MAINTAINABILITY);
22+
static final List<RuleSoftwareQualityAttribute> SOFTWARE_QUALITY_ATTRIBUTES = List
23+
.of(RuleSoftwareQualityAttribute.MAINTAINABILITY);
2224
private static final String IMPROVEMNT_SUB_STRING = " The request should be of type: ";
2325
private boolean isActive;
2426
final String MODEL = "/models/request_model.dat";
27+
private gpt gptClient;
28+
private boolean enableLLM = false;
2529

2630
public RequestTypeDescriptionRule(boolean isActive) {
2731
this.isActive = isActive;
32+
this.gptClient = new gpt();
2833
}
2934

3035
@Override
@@ -62,6 +67,11 @@ public void setIsActive(boolean isActive) {
6267
this.isActive = isActive;
6368
}
6469

70+
@Override
71+
public void setEnableLLM(boolean enableLLM) {
72+
this.enableLLM = enableLLM;
73+
}
74+
6575
@Override
6676
public List<Violation> checkViolation(OpenAPI openAPI) {
6777
RequestMethodsWekaClassifier wt = new RequestMethodsWekaClassifier();
@@ -72,57 +82,103 @@ public List<Violation> checkViolation(OpenAPI openAPI) {
7282
Set<String> paths = openAPI.getPaths().keySet();
7383
Paths pathsTest = openAPI.getPaths();
7484

75-
if (paths.isEmpty()) return violations;
85+
if (paths.isEmpty())
86+
return violations;
7687
pathsTest.values().forEach(pathItem -> {
77-
String keyPath = pathsTest.keySet().stream().filter(key -> pathsTest.get(key).equals(pathItem)).findFirst().get();
78-
if(pathItem.getGet() != null){
88+
String keyPath = pathsTest.keySet().stream().filter(key -> pathsTest.get(key).equals(pathItem)).findFirst()
89+
.get();
90+
if (pathItem.getGet() != null) {
7991
String description = pathItem.getGet().getDescription();
8092
String summary = pathItem.getGet().getSummary();
81-
if(summary != null && !summary.isEmpty()){
82-
getViolationGetRequest(wt, keyPath, summary, ErrorMessage.REQUESTTYPETUNNELINGGET, "get", ImprovementSuggestion.REQUESTTYPEGET, true, violations);
83-
}else if (description != null && !description.isEmpty()){
84-
getViolationGetRequest(wt, keyPath, description, ErrorMessage.REQUESTTYPETUNNELINGGET, "get", ImprovementSuggestion.REQUESTTYPEGET, true, violations);
93+
94+
if (this.enableLLM) {
95+
this.gptClient.tunnelingViolationAI(this, keyPath, description, summary, "get", ImprovementSuggestion.REQUESTTYPEGET, IMPROVEMNT_SUB_STRING, violations);
96+
} else {
97+
if (summary != null && !summary.isEmpty()) {
98+
getViolationGetRequest(wt, keyPath, summary,
99+
ErrorMessage.REQUESTTYPETUNNELINGGET, "get",
100+
ImprovementSuggestion.REQUESTTYPEGET, true, violations);
101+
} else if (description != null && !description.isEmpty()) {
102+
getViolationGetRequest(wt, keyPath, description,
103+
ErrorMessage.REQUESTTYPETUNNELINGGET, "get",
104+
ImprovementSuggestion.REQUESTTYPEGET, true, violations);
105+
}
85106
}
86107

108+
87109
}
88-
if(pathItem.getPost() != null){
110+
if (pathItem.getPost() != null) {
89111
String description = pathItem.getPost().getDescription();
90112
String summary = pathItem.getPost().getSummary();
91-
if(summary != null && !summary.isEmpty()){
92-
getViolationGetRequest(wt, keyPath, summary, ErrorMessage.REQUESTTYPETUNNELINGPOST, "post", ImprovementSuggestion.REQUESTTYPEPOST, true, violations);
93-
}else if (description != null && !description.isEmpty()){
94-
getViolationGetRequest(wt, keyPath, description, ErrorMessage.REQUESTTYPETUNNELINGPOST, "post", ImprovementSuggestion.REQUESTTYPEPOST, true, violations);
113+
114+
if (this.enableLLM) {
115+
this.gptClient.tunnelingViolationAI(this, keyPath, description, summary, "post", ImprovementSuggestion.REQUESTTYPEPOST, IMPROVEMNT_SUB_STRING, violations);
116+
} else {
117+
if (summary != null && !summary.isEmpty()) {
118+
getViolationGetRequest(wt, keyPath, summary,
119+
ErrorMessage.REQUESTTYPETUNNELINGPOST, "post",
120+
ImprovementSuggestion.REQUESTTYPEPOST, true, violations);
121+
} else if (description != null && !description.isEmpty()) {
122+
getViolationGetRequest(wt, keyPath, description,
123+
ErrorMessage.REQUESTTYPETUNNELINGPOST, "post",
124+
ImprovementSuggestion.REQUESTTYPEPOST, true, violations);
125+
}
95126
}
96127
}
97-
if(pathItem.getPut() != null){
128+
if (pathItem.getPut() != null) {
98129
String description = pathItem.getPut().getDescription();
99130
String summary = pathItem.getPut().getSummary();
100-
if(summary != null && !summary.isEmpty()){
101-
getViolationGetRequest(wt, keyPath, summary, "", "put", ImprovementSuggestion.REQUESTTYPEPUT, false, violations);
102-
}else if (description != null && !description.isEmpty()){
103-
getViolationGetRequest(wt, keyPath, description, "", "put", ImprovementSuggestion.REQUESTTYPEPUT, false, violations);
131+
132+
if (this.enableLLM) {
133+
this.gptClient.tunnelingViolationAI(this, keyPath, description, summary, "put", ImprovementSuggestion.REQUESTTYPEPUT, IMPROVEMNT_SUB_STRING, violations);
134+
} else {
135+
if (summary != null && !summary.isEmpty()) {
136+
getViolationGetRequest(wt, keyPath, summary, "", "put",
137+
ImprovementSuggestion.REQUESTTYPEPUT, false,
138+
violations);
139+
} else if (description != null && !description.isEmpty()) {
140+
getViolationGetRequest(wt, keyPath, description, "", "put",
141+
ImprovementSuggestion.REQUESTTYPEPUT,
142+
false, violations);
143+
}
104144
}
105145
}
106-
if(pathItem.getDelete() != null){
146+
if (pathItem.getDelete() != null) {
107147
String description = pathItem.getDelete().getDescription();
108148
String summary = pathItem.getDelete().getSummary();
109-
if(summary != null && !summary.isEmpty()){
110-
getViolationGetRequest(wt, keyPath, summary, "", "delete", ImprovementSuggestion.REQUESTTYPEDELETE, false, violations);
111-
}else if (description != null && !description.isEmpty()){
112-
getViolationGetRequest(wt, keyPath, description, "", "delete", ImprovementSuggestion.REQUESTTYPEDELETE, false, violations);
149+
150+
if (this.enableLLM) {
151+
this.gptClient.tunnelingViolationAI(this, keyPath, description, summary, "delete",
152+
ImprovementSuggestion.REQUESTTYPEDELETE, IMPROVEMNT_SUB_STRING, violations);
153+
} else {
154+
if (summary != null && !summary.isEmpty()) {
155+
getViolationGetRequest(wt, keyPath, summary, "", "delete",
156+
ImprovementSuggestion.REQUESTTYPEDELETE,
157+
false, violations);
158+
} else if (description != null && !description.isEmpty()) {
159+
getViolationGetRequest(wt, keyPath, description, "", "delete",
160+
ImprovementSuggestion.REQUESTTYPEDELETE, false, violations);
161+
}
113162
}
114163
}
115164
});
116165

117166
return violations;
118167
}
119168

120-
private void getViolationGetRequest(RequestMethodsWekaClassifier wt, String keyPath, String description, String requestTypeTunnelingType, String requestType, String requestTypeMessage, boolean switchRequestType, List<Violation> violations) {
169+
private void getViolationGetRequest(RequestMethodsWekaClassifier wt, String keyPath, String description,
170+
String requestTypeTunnelingType, String requestType, String requestTypeMessage, boolean switchRequestType,
171+
List<Violation> violations) {
121172
ImmutablePair<String, Double> predictionValues = wt.predict(description);
122-
if ((predictionValues != null && predictionValues.right != null && predictionValues.left != null) && predictionValues.left.equals("invalid") && (predictionValues.right >= 0.75) && switchRequestType) {
123-
violations.add(new Violation(this, RestAnalyzer.locMapper.getLOCOfPath(keyPath), ImprovementSuggestion.REQUESTTYPETUNELING, keyPath, requestTypeTunnelingType));
124-
} else if ((predictionValues != null && predictionValues.right != null && predictionValues.left != null) && !predictionValues.left.equals(requestType) && (predictionValues.right >= 0.75)) {
125-
violations.add(new Violation(this, RestAnalyzer.locMapper.getLOCOfPath(keyPath), requestTypeMessage + IMPROVEMNT_SUB_STRING + predictionValues.left.toUpperCase(), keyPath, ErrorMessage.REQUESTTYPE));
173+
if ((predictionValues != null && predictionValues.right != null && predictionValues.left != null)
174+
&& predictionValues.left.equals("invalid") && (predictionValues.right >= 0.75) && switchRequestType) {
175+
violations.add(new Violation(this, RestAnalyzer.locMapper.getLOCOfPath(keyPath),
176+
ImprovementSuggestion.REQUESTTYPETUNELING, keyPath, requestTypeTunnelingType));
177+
} else if ((predictionValues != null && predictionValues.right != null && predictionValues.left != null)
178+
&& !predictionValues.left.equals(requestType) && (predictionValues.right >= 0.75)) {
179+
violations.add(new Violation(this, RestAnalyzer.locMapper.getLOCOfPath(keyPath),
180+
requestTypeMessage + IMPROVEMNT_SUB_STRING + predictionValues.left.toUpperCase(), keyPath,
181+
ErrorMessage.REQUESTTYPE));
126182
}
127183
}
128184

src/main/java/cli/utility/Output.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class Output {
1717
private static final String UNDERLINE = "----------------------------------------------";
1818
private final Scanner scanner = new Scanner(System.in);
1919
private boolean useCamelCase = false;
20+
private boolean enableLLM = false;
2021

2122
/**
2223
* Sets which naming convention rules should be active
@@ -26,6 +27,14 @@ public void setNamingConventionRules(boolean useCamelCase) {
2627
this.useCamelCase = useCamelCase;
2728
}
2829

30+
/**
31+
* Toggles flag to enable the usage of LLMs in some rules
32+
* @param enableLLM if true, enables the usage of LLMs
33+
*/
34+
public void setEnableLLM(boolean enableLLM) {
35+
this.enableLLM = enableLLM;
36+
}
37+
2938
/**
3039
* Method for the expert mode. User will be asked to enable or disable each rule. The input will
3140
* be saved in the config file.
@@ -128,6 +137,11 @@ public void startAnalysis(String pathToFile, boolean generateReport) {
128137
List<IRestRule> ruleList = activeRules.getAllRuleObjects();
129138
for (IRestRule rule : ruleList) {
130139
RuleIdentifier ruleIdentifier = rule.getIdentifier();
140+
141+
if (ruleIdentifier == RuleIdentifier.REQUEST_TYPE_DESCRIPTION) {
142+
rule.setEnableLLM(this.enableLLM);
143+
}
144+
131145
if (useCamelCase) {
132146
if (ruleIdentifier == RuleIdentifier.CAMEL_CASE) {
133147
rule.setIsActive(true);
@@ -173,6 +187,11 @@ public void startAnalysis(String pathToFile, String title) {
173187
List<IRestRule> ruleList = activeRules.getAllRuleObjects();
174188
for (IRestRule rule : ruleList) {
175189
RuleIdentifier ruleIdentifier = rule.getIdentifier();
190+
191+
if (ruleIdentifier == RuleIdentifier.REQUEST_TYPE_DESCRIPTION) {
192+
rule.setEnableLLM(this.enableLLM);
193+
}
194+
176195
if (useCamelCase) {
177196
if (ruleIdentifier == RuleIdentifier.CAMEL_CASE) {
178197
rule.setIsActive(true);

0 commit comments

Comments
 (0)