diff --git a/.gitignore b/.gitignore index 7cb31afdd1..80cbfc54d3 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ load-tests/**/Dependencies.toml # Ballerina related ignores Ballerina.lock velocity.log* + +compiler-plugin-tests/**/target diff --git a/changelog.md b/changelog.md index 5673170953..fe07049d9b 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [Add header name mapping support in record fields](https://github.com/ballerina-platform/ballerina-library/issues/7018) - [Introduce util functions to convert query and header record with the `http:Query` and the `http:Header` annotations](https://github.com/ballerina-platform/ballerina-library/issues/7019) - [Migrate client and service data binding lang utils usage into data.jsondata module utils `toJson` and `parserAsType`] (https://github.com/ballerina-platform/ballerina-library/issues/6747) +- [Add static code rules](https://github.com/ballerina-platform/ballerina-library/issues/7283) ### Fixed diff --git a/compiler-plugin-tests/build.gradle b/compiler-plugin-tests/build.gradle index 73c3f3cd25..11b9ca0de0 100644 --- a/compiler-plugin-tests/build.gradle +++ b/compiler-plugin-tests/build.gradle @@ -90,6 +90,7 @@ test { systemProperty "ballerina.offline.flag", "true" useTestNG() finalizedBy jacocoTestReport + testLogging.showStandardStreams = true } jacocoTestReport { diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/ProcessOutputGobbler.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/ProcessOutputGobbler.java new file mode 100644 index 0000000000..462b43de08 --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/ProcessOutputGobbler.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * Helper class to consume the process streams. + */ +class ProcessOutputGobbler implements Runnable { + private final InputStream inputStream; + private final StringBuilder output; + private int exitCode; + + public ProcessOutputGobbler(InputStream inputStream) { + this.inputStream = inputStream; + this.output = new StringBuilder(); + } + + @Override + public void run() { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } catch (IOException e) { + this.output.append(e.getMessage()); + } + } + + public String getOutput() { + return output.toString(); + } + + public int getExitCode() { + return exitCode; + } + + public void setExitCode(int exitCode) { + this.exitCode = exitCode; + } +} diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java new file mode 100644 index 0000000000..07feda9311 --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import org.testng.Assert; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Test; +import org.testng.internal.ExitCode; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Locale; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * This class includes tests for Ballerina Http static code analyzer. + */ +class StaticCodeAnalyzerTest { + + private static final Path RESOURCE_PACKAGES_DIRECTORY = Paths + .get("src", "test", "resources", "static_code_analyzer", "ballerina_packages").toAbsolutePath(); + private static final Path EXPECTED_JSON_OUTPUT_DIRECTORY = Paths. + get("src", "test", "resources", "static_code_analyzer", "expected_output").toAbsolutePath(); + private static final Path BALLERINA_PATH = getBalCommandPath(); + private static final Path JSON_RULES_FILE_PATH = Paths + .get("../", "compiler-plugin", "src", "main", "resources", "rules.json").toAbsolutePath(); + private static final String SCAN_COMMAND = "scan"; + + private static Path getBalCommandPath() { + String balCommand = isWindows() ? "bal.bat" : "bal"; + return Paths.get("../", "target", "ballerina-runtime", "bin", balCommand).toAbsolutePath(); + } + + @BeforeSuite + public void pullScanTool() throws IOException, InterruptedException { + ProcessBuilder processBuilder = new ProcessBuilder(BALLERINA_PATH.toString(), "tool", "pull", SCAN_COMMAND); + ProcessOutputGobbler output = getOutput(processBuilder.start()); + if (Pattern.compile("tool 'scan:.+\\..+\\..+' successfully set as the active version\\.") + .matcher(output.getOutput()).find() || Pattern.compile("tool 'scan:.+\\..+\\..+' is already active\\.") + .matcher(output.getOutput()).find()) { + return; + } + Assert.assertFalse(ExitCode.hasFailure(output.getExitCode())); + } + + @Test + public void validateRulesJson() throws IOException { + String expectedRules = "[" + Arrays.stream(HttpRule.values()) + .map(HttpRule::toString).collect(Collectors.joining(",")) + "]"; + String actualRules = Files.readString(JSON_RULES_FILE_PATH); + assertJsonEqual(normalizeJson(actualRules), normalizeJson(expectedRules)); + } + + @Test + public void testStaticCodeRules() throws IOException, InterruptedException { + for (HttpRule rule : HttpRule.values()) { + String targetPackageName = "rule" + rule.getId(); + String actualJsonReport = StaticCodeAnalyzerTest.executeScanProcess(targetPackageName); + String expectedJsonReport = Files + .readString(EXPECTED_JSON_OUTPUT_DIRECTORY.resolve(targetPackageName + ".json")); + assertJsonEqual(actualJsonReport, expectedJsonReport); + } + } + + public static String executeScanProcess(String targetPackage) throws IOException, InterruptedException { + ProcessBuilder processBuilder = new ProcessBuilder(BALLERINA_PATH.toString(), SCAN_COMMAND); + processBuilder.directory(RESOURCE_PACKAGES_DIRECTORY.resolve(targetPackage).toFile()); + ProcessOutputGobbler output = getOutput(processBuilder.start()); + Assert.assertFalse(ExitCode.hasFailure(output.getExitCode())); + return Files.readString(RESOURCE_PACKAGES_DIRECTORY.resolve(targetPackage) + .resolve("target").resolve("report").resolve("scan_results.json")); + } + + private static ProcessOutputGobbler getOutput(Process process) throws InterruptedException { + ProcessOutputGobbler outputGobbler = new ProcessOutputGobbler(process.getInputStream()); + ProcessOutputGobbler errorGobbler = new ProcessOutputGobbler(process.getErrorStream()); + Thread outputThread = new Thread(outputGobbler); + Thread errorThread = new Thread(errorGobbler); + outputThread.start(); + errorThread.start(); + int exitCode = process.waitFor(); + outputGobbler.setExitCode(exitCode); + errorGobbler.setExitCode(exitCode); + outputThread.join(); + errorThread.join(); + return outputGobbler; + } + + private void assertJsonEqual(String actual, String expected) { + Assert.assertEquals(normalizeJson(actual), normalizeJson(expected)); + } + + private static String normalizeJson(String json) { + String normalizedJson = json.replaceAll("\\s*\"\\s*", "\"") + .replaceAll("\\s*:\\s*", ":") + .replaceAll("\\s*,\\s*", ",") + .replaceAll("\\s*\\{\\s*", "{") + .replaceAll("\\s*}\\s*", "}") + .replaceAll("\\s*\\[\\s*", "[") + .replaceAll("\\s*]\\s*", "]") + .replaceAll("\n", "") + .replaceAll(":\".*module-ballerina-http", ":\"module-ballerina-http"); + return isWindows() ? normalizedJson.replaceAll("/", "\\\\\\\\") : normalizedJson; + } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows"); + } +} diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Ballerina.toml b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Ballerina.toml new file mode 100644 index 0000000000..a2262d50af --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "ballerina" +name = "rule1" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service.bal new file mode 100644 index 0000000000..2490eefb23 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service.bal @@ -0,0 +1,27 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +service on new http:Listener(8080) { + resource function default .() returns string? { + return; + } + + resource function default greet() returns string? { + return; + } +}; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_class.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_class.bal new file mode 100644 index 0000000000..9020df9eef --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_class.bal @@ -0,0 +1,29 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +service class GreetingService { + *http:Service; + + resource function default .() returns string { + return ""; + } + + resource function default greet() returns string { + return ""; + } +}; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal new file mode 100644 index 0000000000..9ede2843d3 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal @@ -0,0 +1,29 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +type ServiceContract service object { + *http:Service; + resource function default .() returns string; + resource function default greet() returns string; +}; + +type MyContract service object { + *http:ServiceContract; + resource function default .() returns string; + resource function default greet() returns string; +}; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/Ballerina.toml b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/Ballerina.toml new file mode 100644 index 0000000000..adf0cc8b06 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "ballerina" +name = "rule2" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service.bal new file mode 100644 index 0000000000..8e095f9d41 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service.bal @@ -0,0 +1,34 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +@http:ServiceConfig { + cors: { + allowOrigins: ["*"] + } +} +service on new http:Listener(8080) { + + @http:ResourceConfig { + cors: { + allowOrigins: ["*"] + } + } + resource function get greet() returns string? { + return; + } +}; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_class.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_class.bal new file mode 100644 index 0000000000..c8779ff6a1 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_class.bal @@ -0,0 +1,30 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +service class GreetingService { + *http:Service; + + @http:ResourceConfig { + cors: { + allowOrigins: ["*"] + } + } + resource function get .() returns string? { + return; + } +}; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal new file mode 100644 index 0000000000..a7c1f6ccd8 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal @@ -0,0 +1,49 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +@http:ServiceConfig { + cors: { + allowOrigins: ["*"] + } +} +type ServiceContract service object { + *http:Service; + + @http:ResourceConfig { + cors: { + allowOrigins: ["*"] + } + } + resource function get greet() returns string; +}; + +@http:ServiceConfig { + cors: { + allowOrigins: ["*"] + } +} +type MyContract service object { + *http:ServiceContract; + + @http:ResourceConfig { + cors: { + allowOrigins: ["*"] + } + } + resource function get greet() returns string; +}; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json new file mode 100644 index 0000000000..0e9afd7895 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json @@ -0,0 +1,162 @@ +[ + { + "location": { + "filePath": "service.bal", + "startLine": 19, + "endLine": 19, + "startColumn": 22, + "endColumn": 29, + "startOffset": 728, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service.bal" + }, + { + "location": { + "filePath": "service.bal", + "startLine": 23, + "endLine": 23, + "startColumn": 22, + "endColumn": 29, + "startOffset": 803, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service.bal" + }, + { + "location": { + "filePath": "service_class.bal", + "startLine": 21, + "endLine": 21, + "startColumn": 22, + "endColumn": 29, + "startOffset": 743, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service_class.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_class.bal" + }, + { + "location": { + "filePath": "service_class.bal", + "startLine": 25, + "endLine": 25, + "startColumn": 22, + "endColumn": 29, + "startOffset": 820, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service_class.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_class.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 20, + "endLine": 20, + "startColumn": 22, + "endColumn": 29, + "startOffset": 748, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 21, + "endLine": 21, + "startColumn": 22, + "endColumn": 29, + "startOffset": 798, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 26, + "endLine": 26, + "startColumn": 22, + "endColumn": 29, + "startOffset": 916, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 27, + "endLine": 27, + "startColumn": 22, + "endColumn": 29, + "startOffset": 966, + "length": 7 + }, + "rule": { + "id": "ballerina/http:1", + "numericId": 1, + "description": "Avoid allowing default resource accessor", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/service_object.bal" + } +] diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json new file mode 100644 index 0000000000..00608dc943 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json @@ -0,0 +1,142 @@ +[ + { + "location": { + "filePath": "service.bal", + "startLine": 20, + "endLine": 20, + "startColumn": 23, + "endColumn": 26, + "startOffset": 726, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service.bal" + }, + { + "location": { + "filePath": "service.bal", + "startLine": 27, + "endLine": 27, + "startColumn": 27, + "endColumn": 30, + "startOffset": 847, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service.bal" + }, + { + "location": { + "filePath": "service_class.bal", + "startLine": 23, + "endLine": 23, + "startColumn": 27, + "endColumn": 30, + "startOffset": 791, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service_class.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_class.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 20, + "endLine": 20, + "startColumn": 23, + "endColumn": 26, + "startOffset": 726, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 28, + "endLine": 28, + "startColumn": 27, + "endColumn": 30, + "startOffset": 867, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 36, + "endLine": 36, + "startColumn": 23, + "endColumn": 26, + "startOffset": 999, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal" + }, + { + "location": { + "filePath": "service_object.bal", + "startLine": 44, + "endLine": 44, + "startColumn": 27, + "endColumn": 30, + "startOffset": 1143, + "length": 3 + }, + "rule": { + "id": "ballerina/http:2", + "numericId": 2, + "description": "Avoid permissive Cross-Origin Resource Sharing", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/service_object.bal", + "filePath": "/Users/admin/Desktop/WORKSPACE/module-ballerina-http/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/service_object.bal" + } +] diff --git a/compiler-plugin/build.gradle b/compiler-plugin/build.gradle index e1c254571c..066c263354 100644 --- a/compiler-plugin/build.gradle +++ b/compiler-plugin/build.gradle @@ -39,6 +39,8 @@ dependencies { implementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" implementation group: 'io.ballerina.openapi', name: 'ballerina-to-openapi', version: "${ballerinaToOpenApiVersion}" + implementation group: 'io.ballerina.scan', name: 'scan-command', version: "${balScanVersion}" + externalJars group: 'io.ballerina.openapi', name: 'ballerina-to-openapi', version: "${ballerinaToOpenApiVersion}" } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java index 640e9c37d9..8e51c2557a 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java @@ -125,4 +125,6 @@ private Constants() {} public static final String SUFFIX_SEPARATOR_REGEX = "\\+"; public static final String MEDIA_TYPE_SUBTYPE_REGEX = "^(\\w)+(\\s*\\.\\s*(\\w)+)*(\\s*\\+\\s*(\\w)+)*"; public static final String UNNECESSARY_CHARS_REGEX = "^'|\"|\\n"; + + public static final String SCANNER_CONTEXT = "ScannerContext"; } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java index 5936bfa6a1..1fd8cb019d 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java @@ -23,6 +23,7 @@ import io.ballerina.projects.plugins.CompilerPluginContext; import io.ballerina.projects.plugins.codeaction.CodeAction; import io.ballerina.projects.plugins.completion.CompletionProvider; +import io.ballerina.scan.ScannerContext; import io.ballerina.stdlib.http.compiler.codeaction.AddHeaderParameterCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddInterceptorRemoteMethodCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddInterceptorResourceMethodCodeAction; @@ -35,10 +36,13 @@ import io.ballerina.stdlib.http.compiler.codeaction.ImplementServiceContract; import io.ballerina.stdlib.http.compiler.codemodifier.HttpServiceModifier; import io.ballerina.stdlib.http.compiler.completion.HttpServiceBodyContextProvider; +import io.ballerina.stdlib.http.compiler.staticcodeanalyzer.HttpStaticCodeAnalyzer; import java.util.List; import java.util.Map; +import static io.ballerina.stdlib.http.compiler.Constants.SCANNER_CONTEXT; + /** * The compiler plugin implementation for Ballerina Http package. */ @@ -52,6 +56,10 @@ public void init(CompilerPluginContext context) { context.addCodeAnalyzer(new HttpServiceAnalyzer(ctxData)); getCodeActions().forEach(context::addCodeAction); getCompletionProviders().forEach(context::addCompletionProvider); + Object object = context.userData().get(SCANNER_CONTEXT); + if (object instanceof ScannerContext scannerContext) { + context.addCodeAnalyzer(new HttpStaticCodeAnalyzer(scannerContext.getReporter())); + } } private List getCodeActions() { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java index 2db9d66682..7142858cb2 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java @@ -20,6 +20,7 @@ import io.ballerina.compiler.api.SemanticModel; import io.ballerina.compiler.api.Types; +import io.ballerina.compiler.api.symbols.ClassSymbol; import io.ballerina.compiler.api.symbols.FunctionSymbol; import io.ballerina.compiler.api.symbols.FunctionTypeSymbol; import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; @@ -32,13 +33,16 @@ import io.ballerina.compiler.api.symbols.TypeSymbol; import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.projects.Document; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.tools.diagnostics.Diagnostic; import io.ballerina.tools.diagnostics.DiagnosticFactory; @@ -47,12 +51,19 @@ import io.ballerina.tools.diagnostics.DiagnosticSeverity; import io.ballerina.tools.diagnostics.Location; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import static io.ballerina.compiler.api.symbols.SymbolKind.TYPE_DEFINITION; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.ANNOTATION; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.AT_TOKEN; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.CLASS_DEFINITION; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.RESOURCE_ACCESSOR_DECLARATION; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.RESOURCE_ACCESSOR_DEFINITION; import static io.ballerina.stdlib.http.compiler.Constants.ANYDATA; import static io.ballerina.stdlib.http.compiler.Constants.ARRAY_OF_MAP_OF_ANYDATA; import static io.ballerina.stdlib.http.compiler.Constants.BALLERINA; @@ -60,8 +71,10 @@ import static io.ballerina.stdlib.http.compiler.Constants.BOOLEAN_ARRAY; import static io.ballerina.stdlib.http.compiler.Constants.BYTE_ARRAY; import static io.ballerina.stdlib.http.compiler.Constants.CALLER_OBJ_NAME; +import static io.ballerina.stdlib.http.compiler.Constants.COLON; import static io.ballerina.stdlib.http.compiler.Constants.DECIMAL; import static io.ballerina.stdlib.http.compiler.Constants.DECIMAL_ARRAY; +import static io.ballerina.stdlib.http.compiler.Constants.DEFAULT; import static io.ballerina.stdlib.http.compiler.Constants.EMPTY; import static io.ballerina.stdlib.http.compiler.Constants.ERROR; import static io.ballerina.stdlib.http.compiler.Constants.FLOAT; @@ -92,6 +105,7 @@ import static io.ballerina.stdlib.http.compiler.Constants.REQUEST_OBJ_NAME; import static io.ballerina.stdlib.http.compiler.Constants.RESOURCE_RETURN_TYPE; import static io.ballerina.stdlib.http.compiler.Constants.RESPONSE_OBJ_NAME; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_KEYWORD; import static io.ballerina.stdlib.http.compiler.Constants.STRING; import static io.ballerina.stdlib.http.compiler.Constants.STRING_ARRAY; import static io.ballerina.stdlib.http.compiler.Constants.STRUCTURED_ARRAY; @@ -348,6 +362,47 @@ public static ServiceDeclarationNode getServiceDeclarationNode(Node node, Semant return serviceDeclarationNode; } + public static ClassDefinitionNode getServiceClassDefinitionNode(SyntaxNodeAnalysisContext ctx) { + if (ctx.node().kind() != CLASS_DEFINITION) { + return null; + } + ClassDefinitionNode classDefinitionNode = (ClassDefinitionNode) ctx.node(); + Optional serviceType = ctx.semanticModel().types() + .getTypeByName(BALLERINA, HTTP, EMPTY, HTTP_SERVICE_TYPE); + if (!hasServiceKeyWord(classDefinitionNode) || serviceType.isEmpty()) { + return null; + } + Optional symbol = ctx.semanticModel().symbol(classDefinitionNode); + if (symbol.isEmpty() || serviceType.get().kind() != TYPE_DEFINITION) { + return null; + } + ClassSymbol classSymbol = (ClassSymbol) symbol.get(); + TypeSymbol serviceTypeSymbol = ((TypeDefinitionSymbol) serviceType.get()).typeDescriptor(); + return classSymbol.subtypeOf(serviceTypeSymbol) ? classDefinitionNode : null; + } + + public static AnnotationNode getAnnotationNode(SyntaxNodeAnalysisContext context) { + if (context.node().kind() != ANNOTATION) { + return null; + } + Optional symbol = context.semanticModel().symbol(context.node()); + if (symbol.isPresent()) { + Optional module = symbol.get().getModule(); + if (module.isEmpty() || !isHttpModule(module.get())) { + return null; + } + return (AnnotationNode) context.node(); + } + // Added as a workaround for: https://github.com/ballerina-platform/ballerina-lang/issues/43525 + return context.node().toSourceCode().trim().startsWith(AT_TOKEN.stringValue() + HTTP + COLON) ? + (AnnotationNode) context.node() : null; + } + + private static boolean hasServiceKeyWord(ClassDefinitionNode classDefinitionNode) { + return classDefinitionNode.classTypeQualifiers() + .stream().anyMatch(token -> SERVICE_KEYWORD.equals(token.text().trim())); + } + private static boolean isListenerBelongsToHttpModule(TypeSymbol listenerType) { if (listenerType.typeKind() == TypeDescKind.UNION) { return ((UnionTypeSymbol) listenerType).memberTypeDescriptors().stream() @@ -378,13 +433,32 @@ public static boolean isHttpServiceType(SemanticModel semanticModel, Node typeNo return false; } - Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + Optional serviceType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, HTTP_SERVICE_TYPE); - if (serviceContractType.isEmpty() || - !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + if (serviceType.isEmpty() || + !(serviceType.get() instanceof TypeDefinitionSymbol serviceTypeDef)) { return false; } + return serviceObjTypeDef.typeDescriptor().subtypeOf(serviceTypeDef.typeDescriptor()); + } + + public static List getResourceMethodWithDefaultAccessor(NodeList members) { + List resourceFunctions = new ArrayList<>(); + for (Node member : members) { + if (member.kind() != RESOURCE_ACCESSOR_DEFINITION && member.kind() != RESOURCE_ACCESSOR_DECLARATION) { + continue; + } + ResourceFunction resourceFunction = member.kind() == RESOURCE_ACCESSOR_DEFINITION + ? new ResourceFunctionDefinition((FunctionDefinitionNode) member) + : new ResourceFunctionDeclaration((MethodDeclarationNode) member); + if (DEFAULT.equalsIgnoreCase(resourceFunction.functionName().text().trim())) { + resourceFunctions.add(resourceFunction); + } + } + return resourceFunctions; + } - return serviceObjTypeDef.typeDescriptor().subtypeOf(serviceContractTypeDef.typeDescriptor()); + public static Document getDocument(SyntaxNodeAnalysisContext context) { + return context.currentPackage().module(context.moduleId()).document(context.documentId()); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpAnnotationAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpAnnotationAnalyzer.java new file mode 100644 index 0000000000..6b12328f52 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpAnnotationAnalyzer.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.projects.Document; +import io.ballerina.projects.plugins.AnalysisTask; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.scan.Reporter; +import io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil; + +import java.util.Optional; +import java.util.regex.Pattern; + +import static io.ballerina.stdlib.http.compiler.staticcodeanalyzer.HttpRule.AVOID_PERMISSIVE_CORS; + +class HttpAnnotationAnalyzer implements AnalysisTask { + private final Reporter reporter; + private static final String CORS_FIELD_NAME = "cors"; + private static final String ALLOW_ORIGINS_FIELD_NAME = "allowOrigins"; + public static final Pattern WILDCARD_ORIGIN = Pattern.compile("\"(\s*)\\*(\s*)\""); + + public HttpAnnotationAnalyzer(Reporter reporter) { + this.reporter = reporter; + } + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + AnnotationNode annotationNode = HttpCompilerPluginUtil.getAnnotationNode(context); + if (annotationNode == null) { + return; + } + Optional annotationValue = annotationNode.annotValue(); + if (annotationValue.isEmpty()) { + return; + } + Document document = HttpCompilerPluginUtil.getDocument(context); + validateAnnotationValue(annotationValue.get(), document); + } + + private void validateAnnotationValue(MappingConstructorExpressionNode annotationValueMap, Document document) { + Optional corsField = findSpecificField(annotationValueMap, CORS_FIELD_NAME); + if (corsField.isEmpty() || corsField.get().valueExpr().isEmpty()) { + return; + } + ExpressionNode corsVal = corsField.get().valueExpr().get(); + if (corsVal.kind() != SyntaxKind.MAPPING_CONSTRUCTOR) { + return; + } + MappingConstructorExpressionNode corsMap = (MappingConstructorExpressionNode) corsVal; + Optional allowOrigins = findSpecificField(corsMap, ALLOW_ORIGINS_FIELD_NAME); + if (allowOrigins.isEmpty() || allowOrigins.get().valueExpr().isEmpty()) { + return; + } + ExpressionNode allowOriginsValue = allowOrigins.get().valueExpr().get(); + if (allowOriginsValue.kind() != SyntaxKind.LIST_CONSTRUCTOR) { + return; + } + checkForPermissiveCors((ListConstructorExpressionNode) allowOriginsValue, document); + } + + private Optional findSpecificField(MappingConstructorExpressionNode mapNode, String fieldName) { + return mapNode.fields().stream() + .filter(field -> field.kind() == SyntaxKind.SPECIFIC_FIELD).map(field -> (SpecificFieldNode) field) + .filter(field -> fieldName.equals(field.fieldName().toSourceCode().trim())).findFirst(); + } + + private void checkForPermissiveCors(ListConstructorExpressionNode allowedOrigins, Document document) { + for (Node exp : allowedOrigins.expressions()) { + if (WILDCARD_ORIGIN.matcher(exp.toSourceCode().trim()).find()) { + this.reporter.reportIssue(document, exp.location(), AVOID_PERMISSIVE_CORS.getId()); + } + } + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceClass.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceClass.java new file mode 100644 index 0000000000..cf3e702382 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceClass.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; + +class HttpHttpServiceClass implements HttpService { + private final ClassDefinitionNode classDefinitionNode; + + public HttpHttpServiceClass(ClassDefinitionNode classDefinitionNode) { + this.classDefinitionNode = classDefinitionNode; + } + + @Override + public NodeList members() { + return this.classDefinitionNode.members(); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceDeclaration.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceDeclaration.java new file mode 100644 index 0000000000..e12a6590fb --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceDeclaration.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; + +class HttpHttpServiceDeclaration implements HttpService { + private final ServiceDeclarationNode serviceDeclarationNode; + + public HttpHttpServiceDeclaration(ServiceDeclarationNode serviceDeclarationNode) { + this.serviceDeclarationNode = serviceDeclarationNode; + } + + @Override + public NodeList members() { + return this.serviceDeclarationNode.members(); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceObjectType.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceObjectType.java new file mode 100644 index 0000000000..5df6a9b0b5 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpHttpServiceObjectType.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; + +class HttpHttpServiceObjectType implements HttpService { + private final ObjectTypeDescriptorNode objectTypeDescriptorNode; + + public HttpHttpServiceObjectType(ObjectTypeDescriptorNode objectTypeDescriptorNode) { + this.objectTypeDescriptorNode = objectTypeDescriptorNode; + } + + @Override + public NodeList members() { + return this.objectTypeDescriptorNode.members(); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpRule.java new file mode 100644 index 0000000000..2bba0a8cc4 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpRule.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.scan.Rule; + +import static io.ballerina.scan.RuleKind.VULNERABILITY; +import static io.ballerina.stdlib.http.compiler.staticcodeanalyzer.RuleFactory.createRule; + +/** + * Represents static code rules specific to the Ballerina Http package. + */ +public enum HttpRule { + AVOID_DEFAULT_RESOURCE_ACCESSOR(createRule(1, "Avoid allowing default resource accessor", VULNERABILITY)), + AVOID_PERMISSIVE_CORS(createRule(2, "Avoid permissive Cross-Origin Resource Sharing", VULNERABILITY)); + + private final Rule rule; + + HttpRule(Rule rule) { + this.rule = rule; + } + + public int getId() { + return this.rule.numericId(); + } + + public Rule getRule() { + return this.rule; + } + + @Override + public String toString() { + return "{\"id\":" + this.getId() + ", \"kind\":\"" + this.rule.kind() + "\"," + + " \"description\" : \"" + this.rule.description() + "\"}"; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpService.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpService.java new file mode 100644 index 0000000000..5696cabe4f --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpService.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; + +public interface HttpService { + NodeList members(); +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpServiceAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpServiceAnalyzer.java new file mode 100644 index 0000000000..f97e9a018e --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpServiceAnalyzer.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.projects.Document; +import io.ballerina.projects.plugins.AnalysisTask; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.scan.Reporter; +import io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil; +import io.ballerina.tools.diagnostics.Location; + +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.isHttpServiceType; +import static io.ballerina.stdlib.http.compiler.staticcodeanalyzer.HttpRule.AVOID_DEFAULT_RESOURCE_ACCESSOR; + +class HttpServiceAnalyzer implements AnalysisTask { + private final Reporter reporter; + + public HttpServiceAnalyzer(Reporter reporter) { + this.reporter = reporter; + } + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + HttpService service = getService(context); + if (service == null) { + return; + } + Document document = HttpCompilerPluginUtil.getDocument(context); + validateServiceMembers(service.members(), document); + } + + private HttpService getService(SyntaxNodeAnalysisContext context) { + return switch (context.node().kind()) { + case SERVICE_DECLARATION -> { + ServiceDeclarationNode serviceDeclarationNode = HttpCompilerPluginUtil + .getServiceDeclarationNode(context); + yield serviceDeclarationNode == null ? null : new HttpHttpServiceDeclaration(serviceDeclarationNode); + } + case OBJECT_TYPE_DESC -> isHttpServiceType(context.semanticModel(), context.node()) ? + new HttpHttpServiceObjectType((ObjectTypeDescriptorNode) context.node()) : null; + case CLASS_DEFINITION -> { + ClassDefinitionNode serviceClassDefinitionNode = HttpCompilerPluginUtil + .getServiceClassDefinitionNode(context); + yield serviceClassDefinitionNode == null ? null : new HttpHttpServiceClass(serviceClassDefinitionNode); + } + default -> null; + }; + } + + private void validateServiceMembers(NodeList members, Document document) { + // TODO: fix location, currently getting always -1 than expected + HttpCompilerPluginUtil.getResourceMethodWithDefaultAccessor(members).forEach(definition -> { + Location accessorLocation = definition.functionName().location(); + this.reporter.reportIssue(document, accessorLocation, AVOID_DEFAULT_RESOURCE_ACCESSOR.getId()); + }); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpStaticCodeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpStaticCodeAnalyzer.java new file mode 100644 index 0000000000..e41e6bdbca --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/HttpStaticCodeAnalyzer.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.projects.plugins.CodeAnalysisContext; +import io.ballerina.projects.plugins.CodeAnalyzer; +import io.ballerina.scan.Reporter; + +import java.util.List; + +import static io.ballerina.compiler.syntax.tree.SyntaxKind.ANNOTATION; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.CLASS_DEFINITION; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.OBJECT_TYPE_DESC; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.SERVICE_DECLARATION; + +/** + * The static code analyzer implementation for Ballerina Http package. + */ +public class HttpStaticCodeAnalyzer extends CodeAnalyzer { + private final Reporter reporter; + + public HttpStaticCodeAnalyzer(Reporter reporter) { + this.reporter = reporter; + } + + @Override + public void init(CodeAnalysisContext analysisContext) { + analysisContext.addSyntaxNodeAnalysisTask(new HttpServiceAnalyzer(reporter), + List.of(SERVICE_DECLARATION, OBJECT_TYPE_DESC, CLASS_DEFINITION)); + analysisContext.addSyntaxNodeAnalysisTask(new HttpAnnotationAnalyzer(reporter), ANNOTATION); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/RuleFactory.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/RuleFactory.java new file mode 100644 index 0000000000..81becd5e16 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/RuleFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.scan.Rule; +import io.ballerina.scan.RuleKind; + +/** + * {@code RuleFactory} contains the logic to create a {@link Rule}. + */ +public class RuleFactory { + public static Rule createRule(int id, String description, RuleKind kind) { + return new RuleImpl(id, description, kind); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/RuleImpl.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/RuleImpl.java new file mode 100644 index 0000000000..c03f2156c6 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/staticcodeanalyzer/RuleImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.compiler.staticcodeanalyzer; + +import io.ballerina.scan.Rule; +import io.ballerina.scan.RuleKind; + +class RuleImpl implements Rule { + private final int id; + private final String description; + private final RuleKind kind; + + RuleImpl(int id, String description, RuleKind kind) { + this.id = id; + this.description = description; + this.kind = kind; + } + + @Override + public String id() { + return Integer.toString(this.id); + } + + @Override + public int numericId() { + return this.id; + } + + @Override + public String description() { + return this.description; + } + + @Override + public RuleKind kind() { + return this.kind; + } +} diff --git a/compiler-plugin/src/main/java/module-info.java b/compiler-plugin/src/main/java/module-info.java index 66d065826f..29131747e7 100644 --- a/compiler-plugin/src/main/java/module-info.java +++ b/compiler-plugin/src/main/java/module-info.java @@ -23,4 +23,5 @@ requires io.swagger.v3.core; requires io.swagger.v3.oas.models; requires io.ballerina.openapi.service; + requires io.ballerina.scan; } diff --git a/compiler-plugin/src/main/resources/rules.json b/compiler-plugin/src/main/resources/rules.json new file mode 100644 index 0000000000..2774fb7821 --- /dev/null +++ b/compiler-plugin/src/main/resources/rules.json @@ -0,0 +1,12 @@ +[ + { + "id": 1, + "kind": "VULNERABILITY", + "description": "Avoid allowing default resource accessor" + }, + { + "id": 2, + "kind": "VULNERABILITY", + "description": "Avoid permissive Cross-Origin Resource Sharing" + } +] diff --git a/gradle.properties b/gradle.properties index 278d734f16..360a9f1bea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,5 +45,7 @@ stdlibDataJsonDataVersion = 0.3.0-20241105-101100-661d11f stdlibJwtVersion=2.13.0 stdlibOAuth2Version=2.12.0 +balScanVersion=0.5.0 + observeVersion=1.3.0 observeInternalVersion=1.3.0