Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ target
# Ballerina
velocity.log*
*Ballerina.lock

compiler-plugin-test/**/target
8 changes: 4 additions & 4 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "file"
version = "1.11.0"
version = "1.11.1"
authors = ["Ballerina"]
keywords = ["file", "path", "directory", "filepath"]
repository = "https://github.com/ballerina-platform/module-ballerina-file"
Expand All @@ -15,12 +15,12 @@ graalvmCompatible = true
[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "file-native"
version = "1.11.0"
path = "../native/build/libs/file-native-1.11.0.jar"
version = "1.11.1"
path = "../native/build/libs/file-native-1.11.1-SNAPSHOT.jar"

[[platform.java21.dependency]]
path = "./lib/org.wso2.transport.local-file-system-6.0.55.jar"

[[platform.java21.dependency]]
path = "../test-utils/build/libs/file-test-utils-1.11.0.jar"
path = "../test-utils/build/libs/file-test-utils-1.11.1-SNAPSHOT.jar"
scope = "testOnly"
2 changes: 1 addition & 1 deletion ballerina/CompilerPlugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ id = "file-compiler-plugin"
class = "io.ballerina.stdlib.file.compiler.FileCompilerPlugin"

[[dependency]]
path = "../compiler-plugin/build/libs/file-compiler-plugin-1.11.0.jar"
path = "../compiler-plugin/build/libs/file-compiler-plugin-1.11.1-SNAPSHOT.jar"
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ distribution-version = "2201.11.0"
[[package]]
org = "ballerina"
name = "file"
version = "1.11.0"
version = "1.11.1"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- [Add static code rules](https://github.com/ballerina-platform/ballerina-library/issues/7283)


### Changed

- [Change the listener configuration as an included parameter](https://github.com/ballerina-platform/ballerina-library/issues/7494)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.file.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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.file.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;

public 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(FileRule.values())
.map(FileRule::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 (FileRule rule : FileRule.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-file", ":\"module-ballerina-file");
return isWindows() ? normalizedJson.replaceAll("/", "\\\\\\\\") : normalizedJson;
}

private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
org = "sachink"
name = "rule1"
version = "0.1.0"
distribution = "2201.11.0-20250127-101700-a4b67fe5"

[build-options]
observabilityIncluded = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

import ballerina/file;
import ballerina/os;
import ballerina/io;

public function main() {
string tempFolderPath = os:getEnv("TMP");
error? output = file:create(tempFolderPath + "/" + "myfile.txt");

if (output is error) {
io:println("Error occurred: " + output.message());
} else {
io:println("File created successfully: " + tempFolderPath + "/" + "myfile.txt");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
org = "sachink"
name = "rule2"
version = "0.1.0"
distribution = "2201.11.0-20250127-101700-a4b67fe5"

[build-options]
observabilityIncluded = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

import ballerina/file;
import ballerina/io;

public function executeCommand(string fileName) returns file:Error? {
string unsafeFilePath = "./target/" + fileName;
file:Error? remove = file:remove(unsafeFilePath);

return remove;
}

public function main() {
file:Error? result = executeCommand("sample.json");
io:println("Result: ", result);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[ {
"location" : {
"filePath" : "main.bal",
"startLine" : 22,
"endLine" : 22,
"startColumn" : 20,
"endColumn" : 68,
"startOffset" : 800,
"length" : 48
},
"rule" : {
"id" : "ballerina/file:1",
"numericId" : 1,
"description" : "Avoid using publicly writable directories for file operations without proper access controls",
"ruleKind" : "VULNERABILITY"
},
"source" : "BUILT_IN",
"fileName" : "rule1/main.bal",
"filePath" : "/Users/sachink/Desktop/module-ballerina-file/compiler-plugin-test/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal"
}, {
"location" : {
"filePath" : "main.bal",
"startLine" : 25,
"endLine" : 25,
"startColumn" : 8,
"endColumn" : 57,
"startOffset" : 886,
"length" : 49
},
"rule" : {
"id" : "ballerina/file:1",
"numericId" : 1,
"description" : "Avoid using publicly writable directories for file operations without proper access controls",
"ruleKind" : "VULNERABILITY"
},
"source" : "BUILT_IN",
"fileName" : "rule1/main.bal",
"filePath" : "/Users/sachink/Desktop/module-ballerina-file/compiler-plugin-test/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal"
}, {
"location" : {
"filePath" : "main.bal",
"startLine" : 27,
"endLine" : 27,
"startColumn" : 8,
"endColumn" : 87,
"startOffset" : 958,
"length" : 79
},
"rule" : {
"id" : "ballerina/file:1",
"numericId" : 1,
"description" : "Avoid using publicly writable directories for file operations without proper access controls",
"ruleKind" : "VULNERABILITY"
},
"source" : "BUILT_IN",
"fileName" : "rule1/main.bal",
"filePath" : "/Users/sachink/Desktop/module-ballerina-file/compiler-plugin-test/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal"
} ]
Loading
Loading