diff --git a/ changelog.md b/ changelog.md index 961e48c6..ec6ff1c5 100644 --- a/ changelog.md +++ b/ changelog.md @@ -5,6 +5,7 @@ This file contains all the notable changes done to the Ballerina TCP package thr ### Added - [Support logging raw template value in log APIs](https://github.com/ballerina-platform/ballerina-library/issues/3331) +- [Add static code rules](https://github.com/ballerina-platform/ballerina-library/issues/7283) ## [2.5.1] - 2023-01-04 diff --git a/.gitignore b/.gitignore index 2e53f73c..a927bbee 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ integration-tests/Dependencies.toml # Ballerina velocity.log* *Ballerina.lock + +compiler-plugin-tests/**/target diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 9a0522db..b1add967 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "log" -version = "2.11.0" +version = "2.11.1" authors = ["Ballerina"] keywords = ["level", "format"] repository = "https://github.com/ballerina-platform/module-ballerina-log" @@ -15,12 +15,18 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "log-native" -version = "2.11.0" -path = "../native/build/libs/log-native-2.11.0.jar" +version = "2.11.1" +path = "../native/build/libs/log-native-2.11.1-SNAPSHOT.jar" + +[[platform.java21.dependency]] +groupId = "io.ballerina.stdlib" +artifactId = "log-compiler-plugin" +version = "2.11.1" +path = "../compiler-plugin/build/libs/log-compiler-plugin-2.11.1-SNAPSHOT.jar" [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "log-test-utils" -version = "2.11.0" -path = "../test-utils/build/libs/log-test-utils-2.11.0.jar" +version = "2.11.1" +path = "../test-utils/build/libs/log-test-utils-2.11.1-SNAPSHOT.jar" scope = "testOnly" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 9dd639d8..9b2ce5eb 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -1,6 +1,6 @@ [plugin] id = "log-compiler-plugin" -class = "io.ballerina.stdlib.log.plugin.LogCompilerPlugin" +class = "io.ballerina.stdlib.log.compiler.LogCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/log-compiler-plugin-2.11.0.jar" +path = "../compiler-plugin/build/libs/log-compiler-plugin-2.11.1-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 4213f4db..3279dc0d 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -76,7 +76,7 @@ modules = [ [[package]] org = "ballerina" name = "log" -version = "2.11.0" +version = "2.11.1" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 7681323d..d6906b0f 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -18,6 +18,12 @@ artifactId = "log-native" version = "@toml.version@" path = "../native/build/libs/log-native-@project.version@.jar" +[[platform.java21.dependency]] +groupId = "io.ballerina.stdlib" +artifactId = "log-compiler-plugin" +version = "@toml.version@" +path = "../compiler-plugin/build/libs/log-compiler-plugin-@project.version@.jar" + [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "log-test-utils" diff --git a/build-config/resources/CompilerPlugin.toml b/build-config/resources/CompilerPlugin.toml index 2db40f06..472dbb75 100644 --- a/build-config/resources/CompilerPlugin.toml +++ b/build-config/resources/CompilerPlugin.toml @@ -1,6 +1,6 @@ [plugin] id = "log-compiler-plugin" -class = "io.ballerina.stdlib.log.plugin.LogCompilerPlugin" +class = "io.ballerina.stdlib.log.compiler.LogCompilerPlugin" [[dependency]] path = "../compiler-plugin/build/libs/log-compiler-plugin-@project.version@.jar" diff --git a/codecov.yml b/codecov.yml index 0e744bb3..ec993452 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,11 @@ fixes: - "ballerina/log/*/::log-ballerina/" + - "io/ballerina/stdlib/log/compiler/::./compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/" ignore: - "**/tests" - "test-utils" + - "compiler-plugin" coverage: precision: 2 diff --git a/compiler-plugin-tests/build.gradle b/compiler-plugin-tests/build.gradle new file mode 100644 index 00000000..51c24ee6 --- /dev/null +++ b/compiler-plugin-tests/build.gradle @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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. + */ + +plugins { + id 'java' + id 'checkstyle' + id 'com.github.spotbugs' + id 'jacoco' +} + +jacoco { + toolVersion = "${jacocoVersion}" + reportsDirectory = file("$project.buildDir/reports/jacoco") +} + +description = 'Ballerina - Log Compiler Plugin Tests' + +dependencies { + checkstyle project(':checkstyle') + checkstyle "com.puppycrawl.tools:checkstyle:${checkstylePluginVersion}" + + implementation project(':log-compiler-plugin') + + testImplementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + testImplementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" + testImplementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" + + implementation group: 'org.testng', name: 'testng', version: "${testngVersion}" +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +sourceCompatibility = JavaVersion.VERSION_21 + +test { + useTestNG() { + suites 'src/test/resources/testng.xml' + } + testLogging.showStandardStreams = true + testLogging { + events "PASSED", "FAILED", "SKIPPED" + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) + } + } + } + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } + sourceSets project(':log-compiler-plugin').sourceSets.main +} + +spotbugsTest { + def classLoader = plugins["com.github.spotbugs"].class.classLoader + def SpotBugsConfidence = classLoader.findLoadedClass("com.github.spotbugs.snom.Confidence") + def SpotBugsEffort = classLoader.findLoadedClass("com.github.spotbugs.snom.Effort") + ignoreFailures = true + effort = SpotBugsEffort.MAX + reportLevel = SpotBugsConfidence.LOW + reportsDir = file("$project.buildDir/reports/spotbugs") + def excludeFile = file("${rootDir}/build-config/spotbugs-exclude.xml") + if (excludeFile.exists()) { + it.excludeFilter = excludeFile + } + reports { + text.enabled = true + } +} + +spotbugsMain { + enabled false +} + +task validateSpotbugs() { + doLast { + if (spotbugsMain.reports.size() > 0 && + spotbugsMain.reports[0].destination.exists() && + spotbugsMain.reports[0].destination.text.readLines().size() > 0) { + spotbugsMain.reports[0].destination?.eachLine { + println 'Failure: ' + it + } + throw new GradleException("Spotbugs rule violations were found."); + } + } +} + +tasks.withType(Checkstyle) { + exclude '**/module-info.java' +} + +checkstyle { + toolVersion "${project.checkstylePluginVersion}" + configFile rootProject.file("build-config/checkstyle/build/checkstyle.xml") + configProperties = ["suppressionFile": file("${rootDir}/build-config/checkstyle/build/suppressions.xml")] +} + +checkstyleMain { + enabled false +} + +spotbugsTest.finalizedBy validateSpotbugs +checkstyleTest.dependsOn ':checkstyle:downloadCheckstyleRuleFiles' + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} + +test.dependsOn ":log-ballerina:build" +build.dependsOn ":log-ballerina:build" diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/ProcessOutputGobbler.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/ProcessOutputGobbler.java new file mode 100644 index 00000000..9dc26dae --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/ProcessOutputGobbler.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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.log.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. + * @since 2.12.0 + */ +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/log/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java new file mode 100644 index 00000000..c5f155d2 --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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.log.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.io.PrintStream; +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. + * @since 2.12.0 + */ +class StaticCodeAnalyzerTest { + + PrintStream outStream = System.out; + + 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(LogRule.values()) + .map(LogRule::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 (LogRule rule : LogRule.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); + } + } + + private 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-log", ":\"module-ballerina-log"); + 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 00000000..efbb839e --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "ballerina" +name = "rule1" +version = "0.1.0" +distribution = "2201.10.3" + +[build-options] +observabilityIncluded = true diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Config.toml b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Config.toml new file mode 100644 index 00000000..fc9c4cd5 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/Config.toml @@ -0,0 +1,2 @@ +password = "xyz" +user = "abc" diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal new file mode 100644 index 00000000..4a126265 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal @@ -0,0 +1,35 @@ +// 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/log; +import ballerina/log as lg; + +public function main() { + log:printInfo(password); + log:printError(string `Error: ${password}`); + log:printWarn(`Error: ${password}`); + log:printError("Error " + password); + log:printWarn("Warning", password = password); + log:printError("Error", password = password, user = user); + lg:printError(password, user = user); +} + +function log() { + log:printInfo("Info"); +} + +configurable string password = ?; +configurable string user = ?; 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 00000000..29138eb9 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json @@ -0,0 +1,182 @@ +[ + { + "location": { + "filePath": "main.bal", + "startLine": 19, + "endLine": 19, + "startColumn": 18, + "endColumn": 26, + "startOffset": 711, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 20, + "endLine": 20, + "startColumn": 36, + "endColumn": 44, + "startOffset": 758, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 21, + "endLine": 21, + "startColumn": 28, + "endColumn": 36, + "startOffset": 799, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 22, + "endLine": 22, + "startColumn": 30, + "endColumn": 38, + "startOffset": 842, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 23, + "endLine": 23, + "startColumn": 40, + "endColumn": 48, + "startOffset": 893, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 24, + "endLine": 24, + "startColumn": 39, + "endColumn": 47, + "startOffset": 943, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 24, + "endLine": 24, + "startColumn": 56, + "endColumn": 60, + "startOffset": 960, + "length": 4 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 25, + "endLine": 25, + "startColumn": 19, + "endColumn": 27, + "startOffset": 986, + "length": 8 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + }, + { + "location": { + "filePath": "main.bal", + "startLine": 25, + "endLine": 25, + "startColumn": 36, + "endColumn": 40, + "startOffset": 1003, + "length": 4 + }, + "rule": { + "id": "ballerina/log:1", + "numericId": 1, + "description": "Potentially-sensitive configurable variables are logged", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/main.bal", + "filePath": "/Users/danesh/ballerina-platform/module-ballerina-log/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal" + } +] diff --git a/compiler-plugin-tests/src/test/resources/testng.xml b/compiler-plugin-tests/src/test/resources/testng.xml new file mode 100644 index 00000000..85e46103 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/testng.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/compiler-plugin/build.gradle b/compiler-plugin/build.gradle index 23a09353..6cffee93 100644 --- a/compiler-plugin/build.gradle +++ b/compiler-plugin/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" - + implementation group: 'io.ballerina.scan', name: 'scan-command', version: "${balScanVersion}" implementation project(":log-native") } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/log/plugin/LogCodeModifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/LogCodeModifier.java similarity index 99% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/log/plugin/LogCodeModifier.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/LogCodeModifier.java index b875183b..8a3089cd 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/log/plugin/LogCodeModifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/LogCodeModifier.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.log.plugin; +package io.ballerina.stdlib.log.compiler; import io.ballerina.compiler.syntax.tree.AbstractNodeFactory; import io.ballerina.compiler.syntax.tree.FunctionArgumentNode; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/log/plugin/LogCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/LogCompilerPlugin.java similarity index 63% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/log/plugin/LogCompilerPlugin.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/LogCompilerPlugin.java index b42bd27a..fc701eb7 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/log/plugin/LogCompilerPlugin.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/LogCompilerPlugin.java @@ -16,18 +16,25 @@ * under the License. */ -package io.ballerina.stdlib.log.plugin; +package io.ballerina.stdlib.log.compiler; import io.ballerina.projects.plugins.CompilerPlugin; import io.ballerina.projects.plugins.CompilerPluginContext; +import io.ballerina.scan.ScannerContext; +import io.ballerina.stdlib.log.compiler.staticcodeanalyzer.StaticCodeAnalyzer; /** * log module Compiler plugin. */ public class LogCompilerPlugin extends CompilerPlugin { + private static final String SCANNER_CONTEXT = "ScannerContext"; + @Override - public void init(CompilerPluginContext compilerPluginContext) { -// compilerPluginContext.addCodeModifier(new LogCodeModifier()); + public void init(CompilerPluginContext context) { + Object object = context.userData().get(SCANNER_CONTEXT); + if (object instanceof ScannerContext scannerContext) { + context.addCodeAnalyzer(new StaticCodeAnalyzer(scannerContext.getReporter())); + } } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/LogRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/LogRule.java new file mode 100644 index 00000000..54eb37c3 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/LogRule.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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.log.compiler.staticcodeanalyzer; + +import io.ballerina.scan.Rule; + +import static io.ballerina.scan.RuleKind.VULNERABILITY; +import static io.ballerina.stdlib.log.compiler.staticcodeanalyzer.RuleFactory.createRule; + +/** + * Enum to hold the log rules. + * @since 2.12.0 + */ +public enum LogRule { + AVOID_LOGGING_CONFIGURABLE_VARIABLES(createRule(1, + "Potentially-sensitive configurable variables are logged", VULNERABILITY)); + private final Rule rule; + + + LogRule(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/log/compiler/staticcodeanalyzer/LogStatementAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/LogStatementAnalyzer.java new file mode 100644 index 00000000..2cbc31e9 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/LogStatementAnalyzer.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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.log.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.VariableSymbol; +import io.ballerina.compiler.syntax.tree.BinaryExpressionNode; +import io.ballerina.compiler.syntax.tree.ChildNodeEntry; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.ExpressionStatementNode; +import io.ballerina.compiler.syntax.tree.ImportOrgNameNode; +import io.ballerina.compiler.syntax.tree.ImportPrefixNode; +import io.ballerina.compiler.syntax.tree.InterpolationNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.NamedArgumentNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.PositionalArgumentNode; +import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; +import io.ballerina.compiler.syntax.tree.TemplateExpressionNode; +import io.ballerina.projects.Document; +import io.ballerina.projects.plugins.AnalysisTask; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.scan.Reporter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static io.ballerina.stdlib.log.compiler.staticcodeanalyzer.LogRule.AVOID_LOGGING_CONFIGURABLE_VARIABLES; + +/** + * This class analyzes the log statements and checks if the log statement is logging a configurable variable. + * + * @since 2.12.0 + */ +public class LogStatementAnalyzer implements AnalysisTask { + + public static final String CONFIGURABLE_QUALIFIER = "CONFIGURABLE"; + public static final String LOG_MODULE = "log"; + public static final String BALLERINA_ORG = "ballerina"; + + final List logFunctions = Arrays.asList("printInfo", "printError", "printWarn"); + + List semanticModels = new ArrayList<>(); + + private final Reporter reporter; + + public LogStatementAnalyzer(Reporter reporter) { + this.reporter = reporter; + } + @Override + public void perform(SyntaxNodeAnalysisContext context) { + // If semantic model is empty, we get semantic models of all the modules in the package and save it in a list + if (semanticModels.isEmpty()) { + context.currentPackage().modules().forEach(module -> { + SemanticModel semanticModel = module.getCompilation().getSemanticModel(); + semanticModels.add(semanticModel); + }); + } + + Document document = context.currentPackage().module(context.moduleId()).document(context.documentId()); + List importPrefix = new ArrayList<>(); + if (document.syntaxTree().rootNode() instanceof ModulePartNode modulePartNode) { + importPrefix = modulePartNode.imports().stream() + .filter(importDeclarationNode -> { + ImportOrgNameNode importOrgNameNode = importDeclarationNode.orgName().orElse(null); + return importOrgNameNode != null && BALLERINA_ORG.equals(importOrgNameNode.orgName().text()); + }) + .filter(importDeclarationNode -> importDeclarationNode.moduleName().stream().anyMatch( + moduleNameNode -> LOG_MODULE.equals(moduleNameNode.text()))) + .map(importDeclarationNode -> { + ImportPrefixNode importPrefixNode = importDeclarationNode.prefix().orElse(null); + return importPrefixNode != null ? importPrefixNode.prefix().text() : LOG_MODULE; + }).toList(); + } + + + if (context.node() instanceof ExpressionStatementNode expressionStatementNode) { + // Check if the log statement has a configurable qualifier + List childlist = expressionStatementNode.expression().childEntries().stream() + .toList(); + + if (childlist.size() < 4) { + return; + } + + Node firstChild = childlist.getFirst().node().orElse(null); + if (firstChild instanceof QualifiedNameReferenceNode qualifiedNameReferenceNode + && importPrefix.contains(qualifiedNameReferenceNode.modulePrefix().text()) + && logFunctions.contains(qualifiedNameReferenceNode.identifier().text())) { + + // The argument of the log function is the third child. second and fourth child are the parentheses + NodeList logArgumentNodeList = childlist.get(2).nodeList(); + // collect all the log function arguments. + List list = logArgumentNodeList.stream().filter(node -> node instanceof PositionalArgumentNode || + node instanceof NamedArgumentNode) + .toList(); + + for (Node node : list) { + if (node instanceof PositionalArgumentNode positionalArgumentNode) { + positionalArgumentNode.childEntries().forEach(childNodeEntry -> { + Node expression = childNodeEntry.node().orElse(null); + if (expression instanceof SimpleNameReferenceNode simpleNameReferenceNode) { + checkConfigurableQualifier(simpleNameReferenceNode, document); + } else if (expression instanceof TemplateExpressionNode templateExpressionNode) { + templateExpressionNode.content().forEach(content -> { + if (content instanceof InterpolationNode interpolationNode) { + interpolationNode.childEntries().forEach(interpolationChild -> { + Node interpolationExpression = interpolationChild.node().orElse(null); + if (interpolationExpression instanceof SimpleNameReferenceNode + simpleNameReferenceNode) { + checkConfigurableQualifier(simpleNameReferenceNode, document); + } + }); + } + }); + } else if (expression instanceof BinaryExpressionNode binaryExpressionNode) { + binaryExpressionNode.childEntries().forEach(childEntry -> { + Node childNode = childEntry.node().orElse(null); + if (childNode instanceof SimpleNameReferenceNode simpleNameReferenceNode) { + checkConfigurableQualifier(simpleNameReferenceNode, document); + } + }); + } + }); + } else if (node instanceof NamedArgumentNode namedArgumentNode) { + checkConfigurableQualifier(namedArgumentNode.expression(), document); + } + } + } + } + } + + private void checkConfigurableQualifier(ExpressionNode argumentNode, Document document) { + semanticModels.forEach(semanticModel -> { + Symbol symbol = semanticModel.symbol(argumentNode).orElse(null); + if (symbol instanceof VariableSymbol variableSymbol) { + variableSymbol.qualifiers().stream().filter(qualifier -> qualifier + .toString().equals(CONFIGURABLE_QUALIFIER)).forEach(qualifier -> { + this.reporter.reportIssue(document, + argumentNode.location(), + AVOID_LOGGING_CONFIGURABLE_VARIABLES.getId()); + }); + } + }); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/RuleFactory.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/RuleFactory.java new file mode 100644 index 00000000..7a68f2a8 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/RuleFactory.java @@ -0,0 +1,33 @@ +/* + * 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.log.compiler.staticcodeanalyzer; + +import io.ballerina.scan.Rule; +import io.ballerina.scan.RuleKind; + +/** + * {@code RuleFactory} contains the logic to create a {@link Rule}. + * + * @since 2.12.0 + */ +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/log/compiler/staticcodeanalyzer/RuleImpl.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/RuleImpl.java new file mode 100644 index 00000000..384690c2 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/RuleImpl.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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.log.compiler.staticcodeanalyzer; + +import io.ballerina.scan.Rule; +import io.ballerina.scan.RuleKind; + +/** + * {@code RuleFactory} contains the logic to create a {@link Rule}. + * + * @since 2.12.0 + */ +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/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/StaticCodeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/StaticCodeAnalyzer.java new file mode 100644 index 00000000..2403eedb --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/log/compiler/staticcodeanalyzer/StaticCodeAnalyzer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). + * + * 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.log.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.CALL_STATEMENT; + +/** + * Static code analyzer for log module. + * + * @since 2.12.0 + */ +public class StaticCodeAnalyzer extends CodeAnalyzer { + private final Reporter reporter; + + public StaticCodeAnalyzer(Reporter reporter) { + this.reporter = reporter; + } + + @Override + public void init(CodeAnalysisContext analysisContext) { + analysisContext.addSyntaxNodeAnalysisTask(new LogStatementAnalyzer(reporter), + List.of(CALL_STATEMENT)); + } +} diff --git a/compiler-plugin/src/main/java/module-info.java b/compiler-plugin/src/main/java/module-info.java index bcfdf10e..74865172 100644 --- a/compiler-plugin/src/main/java/module-info.java +++ b/compiler-plugin/src/main/java/module-info.java @@ -20,4 +20,5 @@ requires io.ballerina.lang; requires io.ballerina.parser; requires io.ballerina.tools.api; + 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 00000000..d335f9f1 --- /dev/null +++ b/compiler-plugin/src/main/resources/rules.json @@ -0,0 +1,7 @@ +[ + { + "id": 1, + "kind": "VULNERABILITY", + "description": "Potentially-sensitive configurable variables are logged" + } +] diff --git a/gradle.properties b/gradle.properties index 42be9e95..48525130 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,8 @@ shadowJarPluginVersion=8.1.1 downloadPluginVersion=5.4.0 releasePluginVersion=2.6.0 ballerinaGradlePluginVersion=2.3.0 +balScanVersion=0.5.0 +jacocoVersion=0.8.10 #stdlib dependencies stdlibIoVersion=1.7.0 diff --git a/settings.gradle b/settings.gradle index eca70c49..3752449f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,7 @@ include ':log-native' include ':log-test-utils' include ':log-ballerina' include ':log-compiler-plugin' +include ':log-compiler-plugin-tests' include ':log-integration-tests' project(':checkstyle').projectDir = file("build-config${File.separator}checkstyle") @@ -46,6 +47,7 @@ project(':log-native').projectDir = file('native') project(':log-test-utils').projectDir = file('test-utils') project(':log-ballerina').projectDir = file('ballerina') project(':log-compiler-plugin').projectDir = file('compiler-plugin') +project(':log-compiler-plugin-tests').projectDir = file('compiler-plugin-tests') project(':log-integration-tests').projectDir = file('integration-tests') gradleEnterprise {