diff --git a/gradle.properties b/gradle.properties index 11d9947b..b8055a05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=io.ballerina.scan -version=0.5.0 +version=0.5.1 # Plugin versions spotbugsPluginVersion=5.0.14 diff --git a/scan-command/build.gradle b/scan-command/build.gradle index 1be5a0b6..5d150928 100644 --- a/scan-command/build.gradle +++ b/scan-command/build.gradle @@ -269,6 +269,7 @@ tasks.test { useTestNG() { suites 'src/test/resources/testng.xml' + testLogging.showStandardStreams = true } } diff --git a/scan-command/src/main/java/io/ballerina/scan/ScannerContext.java b/scan-command/src/main/java/io/ballerina/scan/ScannerContext.java index b9439021..017b856b 100644 --- a/scan-command/src/main/java/io/ballerina/scan/ScannerContext.java +++ b/scan-command/src/main/java/io/ballerina/scan/ScannerContext.java @@ -18,16 +18,20 @@ package io.ballerina.scan; +import java.util.Map; + /** * {@code ScannerContext} represents a context that exposes properties required by scanner plugins from the scan tool. * * @since 0.1.0 - * */ + */ public interface ScannerContext { /** - * Returns the {@link Reporter} to be used to report identified issues. + * Returns the {@link Reporter} to be used to report identified issues. * * @return reporter that needs to be used to report issues identified. - * */ + */ Reporter getReporter(); + + Map userData(); } diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/CoreRule.java b/scan-command/src/main/java/io/ballerina/scan/internal/CoreRule.java index 0990326c..f61ab922 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/CoreRule.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/CoreRule.java @@ -19,20 +19,24 @@ package io.ballerina.scan.internal; import io.ballerina.scan.Rule; -import io.ballerina.scan.RuleKind; import java.util.ArrayList; import java.util.List; +import static io.ballerina.scan.RuleKind.CODE_SMELL; +import static io.ballerina.scan.RuleKind.VULNERABILITY; +import static io.ballerina.scan.internal.RuleFactory.createRule; + /** * {@code CoreRule} contains the core static code analysis rules. * * @since 0.1.0 - * */ + */ enum CoreRule { - AVOID_CHECKPANIC(RuleFactory.createRule(1, "Avoid checkpanic", RuleKind.CODE_SMELL)), - UNUSED_FUNCTION_PARAMETER(RuleFactory.createRule(2, - "Unused function parameter", RuleKind.CODE_SMELL)); + AVOID_CHECKPANIC(createRule(1, "Avoid checkpanic", CODE_SMELL)), + UNUSED_FUNCTION_PARAMETER(createRule(2, "Unused function parameter", CODE_SMELL)), + HARD_CODED_SECRET(createRule(3, "Hard-coded secrets are security-sensitive", VULNERABILITY)), + NON_CONFIGURABLE_SECRET(createRule(4, "Non configurable secrets are security-sensitive", VULNERABILITY)); private final Rule rule; diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/FunctionWithSensitiveParams.java b/scan-command/src/main/java/io/ballerina/scan/internal/FunctionWithSensitiveParams.java new file mode 100644 index 00000000..b5c3e2b3 --- /dev/null +++ b/scan-command/src/main/java/io/ballerina/scan/internal/FunctionWithSensitiveParams.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package io.ballerina.scan.internal; + +import io.ballerina.compiler.api.symbols.Symbol; + +import java.util.List; + +/** + * Represents a function that contains sensitive arguments (e.g., secrets, passwords). + * This record stores the function's symbol and the positions of the arguments + * that are considered sensitive. + * + *

The sensitive parameters are tracked by their positions within the function's + * argument list. This can be used for detecting hardcoded secrets or validating + * configurations of sensitive data passed to the function.

+ * + * @param symbol The symbol representing the function. + * @param sensitiveParamPositions A list of integer positions indicating which + * parameters of the function are considered sensitive. + */ +record FunctionWithSensitiveParams(Symbol symbol, List sensitiveParamPositions) { +} diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java b/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java index 84b7867e..3ae29564 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java @@ -81,7 +81,7 @@ * Represents the project analyzer used for analyzing projects. * * @since 0.1.0 - * */ + */ class ProjectAnalyzer { private final Project project; private final ScanTomlFile scanTomlFile; @@ -103,6 +103,13 @@ class ProjectAnalyzer { List analyze(List inbuiltRules) { ScannerContextImpl scannerContext = new ScannerContextImpl(inbuiltRules); + // Phase 1: Scan the entire codebase and collect necessary user data across documents + project.currentPackage().moduleIds().forEach(moduleId -> { + Module module = project.currentPackage().module(moduleId); + module.documentIds().forEach(scanDocument(module, scannerContext)); + module.testDocumentIds().forEach(scanDocument(module, scannerContext)); + }); + // Phase 2: Analyze the documents individually, which may use the user data collected in the previous phase project.currentPackage().moduleIds().forEach(moduleId -> { Module module = project.currentPackage().module(moduleId); module.documentIds().forEach(analyzeDocument(module, scannerContext)); @@ -111,6 +118,16 @@ List analyze(List inbuiltRules) { return scannerContext.getReporter().getIssues(); } + private Consumer scanDocument(Module module, ScannerContextImpl scannerContext) { + SemanticModel semanticModel = module.getCompilation().getSemanticModel(); + return documentId -> { + Document document = module.document(documentId); + SensitiveParameterTracker scanner = new SensitiveParameterTracker(document.syntaxTree(), + scannerContext, semanticModel); + scanner.scan(); + }; + } + private Consumer analyzeDocument(Module module, ScannerContextImpl scannerContext) { SemanticModel semanticModel = module.getCompilation().getSemanticModel(); return documentId -> { diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/ScannerContextImpl.java b/scan-command/src/main/java/io/ballerina/scan/internal/ScannerContextImpl.java index a35ad595..8a481956 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/ScannerContextImpl.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/ScannerContextImpl.java @@ -21,22 +21,31 @@ import io.ballerina.scan.Rule; import io.ballerina.scan.ScannerContext; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Represents the implementation of the {@link ScannerContext} interface. * * @since 0.1.0 - * */ + */ class ScannerContextImpl implements ScannerContext { private final ReporterImpl reporter; + private final Map userData; ScannerContextImpl(List rules) { - this.reporter = new ReporterImpl(rules); + reporter = new ReporterImpl(rules); + userData = new HashMap<>(); } @Override public ReporterImpl getReporter() { return reporter; } + + @Override + public Map userData() { + return userData; + } } diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/SecretChecker.java b/scan-command/src/main/java/io/ballerina/scan/internal/SecretChecker.java new file mode 100644 index 00000000..f09c659a --- /dev/null +++ b/scan-command/src/main/java/io/ballerina/scan/internal/SecretChecker.java @@ -0,0 +1,317 @@ +/* + * 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. + */ + +package io.ballerina.scan.internal; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Qualifier; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.api.symbols.VariableSymbol; +import io.ballerina.compiler.syntax.tree.BasicLiteralNode; +import io.ballerina.compiler.syntax.tree.ConstantDeclarationNode; +import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionArgumentNode; +import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode; +import io.ballerina.compiler.syntax.tree.MethodCallExpressionNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.ModuleVariableDeclarationNode; +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.NodeVisitor; +import io.ballerina.compiler.syntax.tree.ObjectFieldNode; +import io.ballerina.compiler.syntax.tree.PositionalArgumentNode; +import io.ballerina.compiler.syntax.tree.RecordFieldWithDefaultValueNode; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.compiler.syntax.tree.TemplateExpressionNode; +import io.ballerina.compiler.syntax.tree.Token; +import io.ballerina.compiler.syntax.tree.VariableDeclarationNode; +import io.ballerina.projects.Document; +import io.ballerina.scan.ScannerContext; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import static io.ballerina.scan.internal.CoreRule.HARD_CODED_SECRET; +import static io.ballerina.scan.internal.CoreRule.NON_CONFIGURABLE_SECRET; +import static io.ballerina.scan.internal.SensitiveParameterTracker.FUNCTIONS_WITH_SENSITIVE_PARAMETERS; + +public class SecretChecker extends NodeVisitor { + private static final Pattern SECRET_WORDS = Pattern + .compile("password|passwd|pwd|passphrase|secret|clientSecret|PASSWORD|PASSWD|PWD|PASSPHRASE" + + "|PASS_PHRASE|SECRET|CLIENTSECRET|CLIENT_SECRET|APIKEY|API_KEY"); + private static final Pattern URL_PREFIX = Pattern.compile("^\\w{1,8}://"); + private static final Pattern NON_EMPTY_URL_CREDENTIAL = Pattern.compile("(?[^\\s:]*+):(?\\S++)"); + private static final String PLACE_HOLDER_STRING = "xyz"; + + private final Document document; + private final SyntaxTree syntaxTree; + private final ScannerContext scannerContext; + private final SemanticModel semanticModel; + private final Map functionsWithCredentialParams; + + SecretChecker(Document document, ScannerContext scannerContext, SemanticModel semanticModel) { + this.document = document; + this.syntaxTree = document.syntaxTree(); + this.scannerContext = scannerContext; + this.semanticModel = semanticModel; + this.functionsWithCredentialParams = getFunctionsWithCredentialParams(scannerContext); + } + + private Map getFunctionsWithCredentialParams(ScannerContext scannerContext) { + @SuppressWarnings("unchecked") + Map functionsWithCredentialParams = + (Map) scannerContext.userData() + .getOrDefault(FUNCTIONS_WITH_SENSITIVE_PARAMETERS, new HashMap<>()); + return functionsWithCredentialParams; + } + + public void analyze() { + this.visit((ModulePartNode) syntaxTree.rootNode()); + } + + public static boolean isSecretName(String name) { + return SECRET_WORDS.matcher(name).find(); + } + + @Override + public void visit(DefaultableParameterNode defaultableParameterNode) { + if (isSecretName(defaultableParameterNode.paramName().map(Token::text).orElse("")) + && defaultableParameterNode.expression() instanceof ExpressionNode expressionNode) { + validateExpression(expressionNode); + } + super.visit(defaultableParameterNode); + } + + @Override + public void visit(SpecificFieldNode specificFieldNode) { + if (isSecretName(specificFieldNode.fieldName().toSourceCode())) { + if (specificFieldNode.valueExpr().isEmpty()) { + validateShorthandNotation(specificFieldNode); + } else { + validateExpression(specificFieldNode.valueExpr().get()); + } + } + super.visit(specificFieldNode); + } + + private void validateShorthandNotation(SpecificFieldNode specificFieldNode) { + Optional identifier = semanticModel.symbol(specificFieldNode); + if (identifier.isPresent() && identifier.get().kind() == SymbolKind.VARIABLE) { + VariableSymbol variableSymbol = (VariableSymbol) identifier.get(); + if (!isConfigurable(variableSymbol)) { + reportNonConfigurableSecret(specificFieldNode); + } + } + } + + @Override + public void visit(BasicLiteralNode basicLiteralNode) { + if (basicLiteralNode.kind() == SyntaxKind.STRING_LITERAL) { + String cleanedLiteral = stripQuotes(basicLiteralNode.literalToken().text()); + if (isUrlWithCredentials(cleanedLiteral)) { + reportHardCodedSecret(basicLiteralNode); + } + } + super.visit(basicLiteralNode); + } + + private String stripQuotes(String text) { + return text.length() > 1 ? text.substring(1, text.length() - 1) : text; + } + + private boolean isUrlWithCredentials(String stringLiteral) { + if (URL_PREFIX.matcher(stringLiteral).find()) { + try { + String userInfo = new URL(stringLiteral).getUserInfo(); + return userInfo != null && NON_EMPTY_URL_CREDENTIAL.matcher(userInfo).matches(); + } catch (MalformedURLException ignored) { + // Ignore since this not a valid url + } + } + return false; + } + + @Override + public void visit(TemplateExpressionNode templateExpressionNode) { + if (templateExpressionNode.kind() == SyntaxKind.STRING_TEMPLATE_EXPRESSION) { + String pattern = buildStringPattern(templateExpressionNode.content()); + if (isUrlWithCredentials(pattern)) { + reportHardCodedSecret(templateExpressionNode); + } + } + super.visit(templateExpressionNode); + } + + private String buildStringPattern(NodeList content) { + StringBuilder pattern = new StringBuilder(); + for (Node node : content) { + if (node.kind() == SyntaxKind.TEMPLATE_STRING) { + pattern.append(node.toSourceCode()); + } else { + pattern.append(PLACE_HOLDER_STRING); + } + } + return pattern.toString(); + } + + @Override + public void visit(ModuleVariableDeclarationNode moduleVariableDeclarationNode) { + if (isSecretName(moduleVariableDeclarationNode.typedBindingPattern().bindingPattern().toSourceCode()) + && moduleVariableDeclarationNode.initializer().isPresent()) { + validateExpression(moduleVariableDeclarationNode.initializer().get()); + } + super.visit(moduleVariableDeclarationNode); + } + + @Override + public void visit(VariableDeclarationNode variableDeclarationNode) { + if (isSecretName(variableDeclarationNode.typedBindingPattern().bindingPattern().toSourceCode()) + && variableDeclarationNode.initializer().isPresent()) { + validateExpression(variableDeclarationNode.initializer().get()); + } + super.visit(variableDeclarationNode); + } + + @Override + public void visit(NamedArgumentNode namedArgumentNode) { + if (isSecretName(namedArgumentNode.argumentName().name().text())) { + validateExpression(namedArgumentNode.expression()); + } + super.visit(namedArgumentNode); + } + + @Override + public void visit(RecordFieldWithDefaultValueNode recordFieldWithDefaultValueNode) { + if (isSecretName(recordFieldWithDefaultValueNode.fieldName().text().trim())) { + validateExpression(recordFieldWithDefaultValueNode.expression()); + } + super.visit(recordFieldWithDefaultValueNode); + } + + @Override + public void visit(ObjectFieldNode objectFieldNode) { + if (isSecretName(objectFieldNode.fieldName().text().trim()) + && objectFieldNode.expression().isPresent()) { + validateExpression(objectFieldNode.expression().get()); + } + super.visit(objectFieldNode); + } + + @Override + public void visit(FunctionCallExpressionNode functionCallExpressionNode) { + validateFunctionCall(functionCallExpressionNode, functionCallExpressionNode.arguments()); + super.visit(functionCallExpressionNode); + } + + @Override + public void visit(ConstantDeclarationNode constantDeclarationNode) { + if (isSecretName(constantDeclarationNode.variableName().text())) { + Node initializer = constantDeclarationNode.initializer(); + if (initializer instanceof ExpressionNode expressionNode) { + validateExpression(expressionNode); + } + } + super.visit(constantDeclarationNode); + } + + @Override + public void visit(MethodCallExpressionNode methodCallExpressionNode) { + validateFunctionCall(methodCallExpressionNode, methodCallExpressionNode.arguments()); + super.visit(methodCallExpressionNode); + } + + private void validateFunctionCall(Node functionNode, NodeList arguments) { + Optional functionSymbol = semanticModel.symbol(functionNode); + functionSymbol.map(Symbol::hashCode) + .filter(functionsWithCredentialParams::containsKey) + .ifPresent(hashCode -> validateFunctionArguments(arguments, functionsWithCredentialParams.get(hashCode) + .sensitiveParamPositions())); + } + + private void validateFunctionArguments(NodeList arguments, + List sensitiveParameterPositions) { + int currentPos = 0; + Iterator targetPositions = sensitiveParameterPositions.iterator(); + int targetPos = targetPositions.hasNext() ? targetPositions.next() : -1; + + for (FunctionArgumentNode arg : arguments) { + if (arg.kind() != SyntaxKind.POSITIONAL_ARG || targetPos == -1) { + return; + } + + if (currentPos == targetPos) { + validateArgument((PositionalArgumentNode) arg); + targetPos = targetPositions.hasNext() ? targetPositions.next() : -1; + } + currentPos++; + } + } + + private void validateArgument(PositionalArgumentNode argumentNode) { + validateExpression(argumentNode.expression()); + } + + private void validateExpression(ExpressionNode expressionNode) { + switch (expressionNode.kind()) { + case STRING_LITERAL -> reportHardCodedSecret(expressionNode); + case SIMPLE_NAME_REFERENCE -> validateSimpleNameReference(expressionNode); + default -> reportNonConfigurableSecret(expressionNode); + } + } + + private void reportHardCodedSecret(Node node) { + scannerContext.getReporter().reportIssue(document, node.location(), HARD_CODED_SECRET.rule()); + } + + private void reportNonConfigurableSecret(Node node) { + scannerContext.getReporter().reportIssue(document, node.location(), NON_CONFIGURABLE_SECRET.rule()); + } + + private void validateSimpleNameReference(ExpressionNode expressionNode) { + Optional identifier = semanticModel.symbol(expressionNode); + if (identifier.isEmpty()) { + return; + } + if (identifier.get().kind() == SymbolKind.VARIABLE) { + VariableSymbol variableSymbol = (VariableSymbol) identifier.get(); + if (!isConfigurable(variableSymbol)) { + reportNonConfigurableSecret(expressionNode); + } + return; + } + if (identifier.get().kind() == SymbolKind.CONSTANT) { + reportHardCodedSecret(expressionNode); + } + } + + private boolean isConfigurable(VariableSymbol variableSymbol) { + return variableSymbol.qualifiers().stream() + .anyMatch(qualifier -> Qualifier.CONFIGURABLE.name().equals(qualifier.name().trim())); + } +} diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/SensitiveParameterTracker.java b/scan-command/src/main/java/io/ballerina/scan/internal/SensitiveParameterTracker.java new file mode 100644 index 00000000..d5d0daa1 --- /dev/null +++ b/scan-command/src/main/java/io/ballerina/scan/internal/SensitiveParameterTracker.java @@ -0,0 +1,130 @@ +/* + * 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. + */ + +package io.ballerina.scan.internal; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.NodeVisitor; +import io.ballerina.compiler.syntax.tree.ParameterNode; +import io.ballerina.compiler.syntax.tree.RequiredParameterNode; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.compiler.syntax.tree.Token; +import io.ballerina.scan.ScannerContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class SensitiveParameterTracker extends NodeVisitor { + public static final String FUNCTIONS_WITH_SENSITIVE_PARAMETERS = "functionsWithSensitiveParameters"; + + private final SyntaxTree syntaxTree; + private final SemanticModel semanticModel; + private final Map userData; + + SensitiveParameterTracker(SyntaxTree syntaxTree, ScannerContext scannerContext, SemanticModel semanticModel) { + this.syntaxTree = syntaxTree; + this.semanticModel = semanticModel; + this.userData = scannerContext.userData(); + } + + public void scan() { + if (!userData.containsKey(FUNCTIONS_WITH_SENSITIVE_PARAMETERS)) { + userData.put(FUNCTIONS_WITH_SENSITIVE_PARAMETERS, new HashMap()); + } + this.visit((ModulePartNode) syntaxTree.rootNode()); + } + + @Override + public void visit(MethodDeclarationNode methodDeclarationNode) { + SeparatedNodeList parameters = methodDeclarationNode.methodSignature().parameters(); + List sensitiveParamsPositions = getSensitiveParameterPositions(parameters); + if (sensitiveParamsPositions.isEmpty()) { + super.visit(methodDeclarationNode); + return; + } + Optional functionSymbol = this.semanticModel.symbol(methodDeclarationNode); + if (functionSymbol.isEmpty()) { + super.visit(methodDeclarationNode); + return; + } + @SuppressWarnings("unchecked") + Map functionWithSensitiveParams = + (Map) userData.get(FUNCTIONS_WITH_SENSITIVE_PARAMETERS); + functionWithSensitiveParams.put(functionSymbol.get().hashCode(), + new FunctionWithSensitiveParams(functionSymbol.get(), sensitiveParamsPositions)); + super.visit(methodDeclarationNode); + } + + private List getSensitiveParameterPositions(SeparatedNodeList parameters) { + List positions = new ArrayList<>(); + int position = 0; + for (ParameterNode parameter : parameters) { + String paramName = getParameterName(parameter); + if (SecretChecker.isSecretName(paramName)) { + positions.add(position); + } + position++; + } + return positions; + } + + private String getParameterName(ParameterNode parameter) { + if (parameter.kind() == SyntaxKind.DEFAULTABLE_PARAM) { + return ((DefaultableParameterNode) parameter).paramName().map(Token::text) + .orElse(parameter.toSourceCode().trim()); + } + if (parameter.kind() == SyntaxKind.REQUIRED_PARAM) { + return ((RequiredParameterNode) parameter).paramName().map(Token::text) + .orElse(parameter.toSourceCode().trim()); + } + return parameter.toSourceCode().trim(); + } + + @Override + public void visit(FunctionDefinitionNode functionDefinitionNode) { + SeparatedNodeList parameters = functionDefinitionNode.functionSignature().parameters(); + List sensitiveParameterPositions = getSensitiveParameterPositions(parameters); + if (sensitiveParameterPositions.isEmpty()) { + super.visit(functionDefinitionNode); + return; + } + Optional functionSymbol = this.semanticModel.symbol(functionDefinitionNode); + if (functionSymbol.isEmpty()) { + super.visit(functionDefinitionNode); + return; + } + @SuppressWarnings("unchecked") + Map map = (Map) + userData.get(FUNCTIONS_WITH_SENSITIVE_PARAMETERS); + map.put(functionSymbol.get().hashCode(), + new FunctionWithSensitiveParams(functionSymbol.get(), sensitiveParameterPositions)); + super.visit(functionDefinitionNode); + } +} + + diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/StaticCodeAnalyzer.java b/scan-command/src/main/java/io/ballerina/scan/internal/StaticCodeAnalyzer.java index 782667d5..67768421 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/StaticCodeAnalyzer.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/StaticCodeAnalyzer.java @@ -58,6 +58,8 @@ class StaticCodeAnalyzer extends NodeVisitor { void analyze() { this.visit((ModulePartNode) syntaxTree.rootNode()); + SecretChecker secretChecker = new SecretChecker(document, scannerContext, semanticModel); + secretChecker.analyze(); } /** diff --git a/scan-command/src/test/java/io/ballerina/scan/BaseTest.java b/scan-command/src/test/java/io/ballerina/scan/BaseTest.java index 6d5a3f4f..41b560e4 100644 --- a/scan-command/src/test/java/io/ballerina/scan/BaseTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/BaseTest.java @@ -39,7 +39,7 @@ * @since 0.1.0 */ public abstract class BaseTest { - protected final Path testResources = Paths.get("src", "test", "resources"); + protected static final Path TEST_RESOURCES = Paths.get("src", "test", "resources"); private ByteArrayOutputStream console; protected PrintStream printStream; protected final String userDir = System.getProperty("user.dir"); @@ -70,9 +70,9 @@ protected String readOutput(boolean silent) throws IOException { } protected String getExpectedOutput(String path) throws IOException { - Path expectedFilePath = testResources.resolve("command-outputs").resolve("common").resolve(path); + Path expectedFilePath = TEST_RESOURCES.resolve("command-outputs").resolve("common").resolve(path); if (!expectedFilePath.toFile().isFile()) { - expectedFilePath = testResources.resolve("command-outputs") + expectedFilePath = TEST_RESOURCES.resolve("command-outputs") .resolve(OsUtils.isWindows() ? "windows" : "unix").resolve(path); } return Files.readString(expectedFilePath, StandardCharsets.UTF_8) diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/CoreRuleTest.java b/scan-command/src/test/java/io/ballerina/scan/internal/CoreRuleTest.java index 1dd3c15f..1ee649a9 100644 --- a/scan-command/src/test/java/io/ballerina/scan/internal/CoreRuleTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/internal/CoreRuleTest.java @@ -29,12 +29,10 @@ * @since 0.1.0 */ public class CoreRuleTest { - public static final String AVOID_CHECKPANIC = "Avoid checkpanic"; - public static final String UNUSED_FUNCTION_PARAMETER = "Unused function parameter"; @Test(description = "test all rules") void testAllRules() { - Assert.assertEquals(CoreRule.rules().size(), 2); + Assert.assertEquals(CoreRule.rules().size(), 4); } @Test(description = "test checkpanic rule") @@ -42,16 +40,34 @@ void testCheckpanicRule() { Rule rule = CoreRule.AVOID_CHECKPANIC.rule(); Assert.assertEquals(rule.id(), "ballerina:1"); Assert.assertEquals(rule.numericId(), 1); - Assert.assertEquals(rule.description(), AVOID_CHECKPANIC); + Assert.assertEquals(rule.description(), "Avoid checkpanic"); Assert.assertEquals(rule.kind(), RuleKind.CODE_SMELL); } - @Test(description = "test unused function parameters test") + @Test(description = "test unused function parameters") void testUnusedFunctionParameterRule() { Rule rule = CoreRule.UNUSED_FUNCTION_PARAMETER.rule(); Assert.assertEquals(rule.id(), "ballerina:2"); Assert.assertEquals(rule.numericId(), 2); - Assert.assertEquals(rule.description(), UNUSED_FUNCTION_PARAMETER); + Assert.assertEquals(rule.description(), "Unused function parameter"); Assert.assertEquals(rule.kind(), RuleKind.CODE_SMELL); } + + @Test(description = "test hard coded secret") + void testHadCodedSecretRule() { + Rule rule = CoreRule.HARD_CODED_SECRET.rule(); + Assert.assertEquals(rule.id(), "ballerina:3"); + Assert.assertEquals(rule.numericId(), 3); + Assert.assertEquals(rule.description(), "Hard-coded secrets are security-sensitive"); + Assert.assertEquals(rule.kind(), RuleKind.VULNERABILITY); + } + + @Test(description = "test non configurable coded secret") + void testNonConfigurableSecretRule() { + Rule rule = CoreRule.NON_CONFIGURABLE_SECRET.rule(); + Assert.assertEquals(rule.id(), "ballerina:4"); + Assert.assertEquals(rule.numericId(), 4); + Assert.assertEquals(rule.description(), "Non configurable secrets are security-sensitive"); + Assert.assertEquals(rule.kind(), RuleKind.VULNERABILITY); + } } diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/Location.java b/scan-command/src/test/java/io/ballerina/scan/internal/Location.java new file mode 100644 index 00000000..6bf31b86 --- /dev/null +++ b/scan-command/src/test/java/io/ballerina/scan/internal/Location.java @@ -0,0 +1,4 @@ +package io.ballerina.scan.internal; + +public record Location(int statLine, int startOffset, int endLine, int endOffset) { +} diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/ProjectAnalyzerTest.java b/scan-command/src/test/java/io/ballerina/scan/internal/ProjectAnalyzerTest.java index 2fa55a04..5b9ef8da 100644 --- a/scan-command/src/test/java/io/ballerina/scan/internal/ProjectAnalyzerTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/internal/ProjectAnalyzerTest.java @@ -51,7 +51,7 @@ * @since 0.1.0 */ public class ProjectAnalyzerTest extends BaseTest { - private final Project project = BuildProject.load(testResources.resolve("test-resources") + private final Project project = BuildProject.load(TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-config-file")); private ProjectAnalyzer projectAnalyzer = null; @@ -153,7 +153,7 @@ private void assertIssue(Issue issue, String fileName, int startLine, int startO @Test(description = "Test analyzing project with invalid external analyzer rules.json configurations") void testAnalyzingProjectWithInvalidExternalAnalyzerRules() throws IOException { - Project invalidProject = BuildProject.load(testResources.resolve("test-resources") + Project invalidProject = BuildProject.load(TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-external-analyzer-rules")); System.setProperty("user.dir", invalidProject.sourceRoot().toString()); ScanTomlFile scanTomlFile = ScanUtils.loadScanTomlConfigurations(invalidProject, printStream) @@ -175,7 +175,7 @@ void testAnalyzingProjectWithInvalidExternalAnalyzerRules() throws IOException { @Test(description = "Test analyzing project with invalid external analyzer rule format") void testAnalyzingProjectWithInvalidExternalAnalyzerRuleFormat() throws IOException { - Project invalidProject = BuildProject.load(testResources.resolve("test-resources") + Project invalidProject = BuildProject.load(TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-external-analyzer-rule-format")); System.setProperty("user.dir", invalidProject.sourceRoot().toString()); ScanTomlFile scanTomlFile = ScanUtils.loadScanTomlConfigurations(invalidProject, printStream) @@ -197,7 +197,7 @@ void testAnalyzingProjectWithInvalidExternalAnalyzerRuleFormat() throws IOExcept @Test(description = "Test analyzing project with invalid external analyzer rule kind") void testAnalyzingProjectWithInvalidExternalAnalyzerRuleKind() throws IOException { - Project invalidProject = BuildProject.load(testResources.resolve("test-resources") + Project invalidProject = BuildProject.load(TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-external-analyzer-rule-kind")); System.setProperty("user.dir", invalidProject.sourceRoot().toString()); ScanTomlFile scanTomlFile = ScanUtils.loadScanTomlConfigurations(invalidProject, printStream) diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/Rule003Test.java b/scan-command/src/test/java/io/ballerina/scan/internal/Rule003Test.java new file mode 100644 index 00000000..e55a5048 --- /dev/null +++ b/scan-command/src/test/java/io/ballerina/scan/internal/Rule003Test.java @@ -0,0 +1,189 @@ +/* + * 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. + */ + +package io.ballerina.scan.internal; + +import io.ballerina.scan.Issue; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.ballerina.scan.internal.CoreRule.HARD_CODED_SECRET; + +/** + * Hard-coded secret analyzer tests. + * + * @since 0.1.0 + */ +public class Rule003Test extends StaticCodeAnalyzerTest { + @Test + void testHardcodedModuleLevelSecretVariable() { + String documentName = "rule003_hardcoded_module_level_secret_variable.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 6); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(16, 18, 16, 28), + new Location(17, 16, 17, 24), + new Location(18, 13, 18, 18), + new Location(19, 20, 19, 32), + new Location(20, 16, 20, 24), + new Location(21, 22, 21, 36))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedLocalSecretVariable() { + String documentName = "rule003_hardcoded_local_secret_variable.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 6); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(17, 22, 17, 32), + new Location(18, 20, 18, 28), + new Location(19, 17, 19, 22), + new Location(20, 24, 20, 36), + new Location(21, 20, 21, 28), + new Location(22, 26, 22, 40))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedDefaultRecordField() { + String documentName = "rule003_hardcoded_default_record_field_value.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(18, 22, 18, 32), + new Location(23, 26, 23, 34))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedSecretRecordFieldInArgument() { + String documentName = "rule003_hardcoded_secret_record_field_in_argument.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 3); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(28, 70, 28, 78), + new Location(30, 57, 30, 65), + new Location(31, 89, 31, 94))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedSecretFunctionArgument() { + String documentName = "rules003_hardcoded_secret_function_argument.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 7); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(19, 38, 19, 48), + new Location(20, 49, 20, 57), + new Location(23, 36, 23, 44), + new Location(24, 47, 24, 55), + new Location(27, 41, 27, 51), + new Location(44, 37, 44, 45), + new Location(45, 59, 45, 67))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedSecretObjectFields() { + String documentName = "rules003_hardcoded_secret_object_field.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(17, 26, 17, 42), + new Location(18, 23, 18, 36) + )); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedSecretMapField() { + String documentName = "rules003_hardcode_secret_for_map_field.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 3); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(28, 60, 28, 70), + new Location(29, 74, 29, 79), + new Location(30, 56, 30, 66))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedDefaultSecretParameter() { + String documentName = "rules003_hardcoded_default_secret_parameter.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(18, 58, 18, 75), + new Location(23, 55, 23, 63))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testUrlWithHardcodedCredentials() { + String documentName = "rules003_url_with_hardcoded_credentials.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(16, 27, 16, 65), + new Location(20, 31, 20, 89))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedSecretConstant() { + String documentName = "rule003_hardcoded_secret_constant.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(16, 17, 16, 27), + new Location(17, 16, 17, 51))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } + + @Test + void testHardcodedConstantAssignment() { + String documentName = "rule003_hardcoded_constant_assignment.bal"; + List issues = analyze(documentName, List.of(HARD_CODED_SECRET.rule())); + Assert.assertEquals(issues.size(), 8); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(19, 28, 19, 66), + new Location(23, 22, 23, 35), + new Location(31, 70, 31, 83), + new Location(33, 57, 33, 70), + new Location(34, 89, 34, 102), + new Location(35, 56, 35, 69), + new Location(59, 58, 59, 71), + new Location(64, 55, 64, 68))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), HARD_CODED_SECRET.rule())); + } +} diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/Rule004Test.java b/scan-command/src/test/java/io/ballerina/scan/internal/Rule004Test.java new file mode 100644 index 00000000..52b3ddd5 --- /dev/null +++ b/scan-command/src/test/java/io/ballerina/scan/internal/Rule004Test.java @@ -0,0 +1,155 @@ +/* + * 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. + */ + +package io.ballerina.scan.internal; + +import io.ballerina.scan.Issue; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.ballerina.scan.internal.CoreRule.HARD_CODED_SECRET; +import static io.ballerina.scan.internal.CoreRule.NON_CONFIGURABLE_SECRET; + +/** + * Non-configurable secret analyzer tests. + * + * @since 0.1.0 + */ +public class Rule004Test extends StaticCodeAnalyzerTest { + @Test + void testNonConfigVariableAssignmentToSecret() { + String documentName = "rule004_non_config_variable_assignment_to_secret.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 6); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(19, 22, 19, 34), + new Location(20, 20, 20, 32), + new Location(21, 17, 21, 29), + new Location(22, 24, 22, 36), + new Location(23, 20, 23, 32), + new Location(24, 26, 24, 38))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigVariableAssignmentToDefaultRecordField() { + String documentName = "rule004_non_config_variable_assignment_to_default_record_field.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(20, 22, 20, 34), + new Location(25, 26, 25, 38))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigVariableAssignmentToRecordFieldArgument() { + String documentName = "rule004_non_config_variable_assignment_to_record_field_argument.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 3); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(30, 70, 30, 82), + new Location(32, 57, 32, 69), + new Location(33, 89, 33, 101))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigSecretAsFunctionArgument() { + String documentName = "rules004_non_config_secret_as_function_argument.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 7); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(21, 38, 21, 50), + new Location(22, 49, 22, 61), + new Location(25, 36, 25, 48), + new Location(26, 47, 26, 59), + new Location(29, 41, 29, 53), + new Location(46, 37, 46, 49), + new Location(47, 59, 47, 71) + )); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigSecretInMapField() { + String documentName = "rules004_non_config_secret_in_map_field.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 3); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(30, 60, 30, 72), + new Location(31, 74, 31, 86), + new Location(32, 56, 32, 68) + )); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigVariableAssignmentToDefaultParameter() { + String documentName = "rules004_non_config_variable_assignment_to_default_parameter.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(20, 58, 20, 70), + new Location(25, 55, 25, 67))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigExpressionAssignmentToVariable() { + String documentName = "rules004_non_config_expression.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 1); + List expectedIssueLocations = new ArrayList<>( + List.of(new Location(20, 22, 20, 27))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigVariableAssignmentToObjectField() { + String documentName = "rules004_non_config_variable_assignment_to_object_field.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + List expectedIssueLocations = new ArrayList<>(List.of( + new Location(19, 26, 19, 38), + new Location(20, 28, 20, 40))); + issues.forEach(issue -> assertIssue(issue, documentName, + expectedIssueLocations.remove(0), NON_CONFIGURABLE_SECRET.rule())); + } + + @Test + void testNonConfigValueInShortHandFieldNotation() { + String documentName = "rules004_non_config_value_in_short_hand_field.bal"; + List issues = analyze(documentName, List.of(NON_CONFIGURABLE_SECRET.rule())); + Assert.assertEquals(issues.size(), 2); + assertIssue(issues.remove(0), documentName, new Location(16, 18, 16, 28), + HARD_CODED_SECRET.rule()); + assertIssue(issues.remove(0), documentName, new Location(24, 50, 24, 58), + NON_CONFIGURABLE_SECRET.rule()); + } +} diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java b/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java index 1a4eed5d..cb1d8775 100644 --- a/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java @@ -58,7 +58,7 @@ * @since 0.1.0 */ public class ScanCmdTest extends BaseTest { - private final Path validBalProject = testResources.resolve("test-resources") + private final Path validBalProject = TEST_RESOURCES.resolve("test-resources") .resolve("valid-bal-project"); private static final String RESULTS_DIRECTORY = "results"; @@ -115,7 +115,7 @@ void testScanCommandProject() throws IOException { @Test(description = "test scan command with an empty Ballerina project") void testScanCommandEmptyProject() throws IOException { - Path emptyBalProject = testResources.resolve("test-resources").resolve("empty-bal-project"); + Path emptyBalProject = TEST_RESOURCES.resolve("test-resources").resolve("empty-bal-project"); ScanCmd scanCmd = new ScanCmd(printStream); System.setProperty("user.dir", emptyBalProject.toString()); scanCmd.execute(); @@ -137,7 +137,7 @@ void testScanCommandProjectWithArgument() throws IOException { @Test(description = "test scan command with single file project with single file as argument") void testScanCommandSingleFileProject() throws IOException { - Path validBalProject = testResources.resolve("test-resources").resolve("valid-single-file-project"); + Path validBalProject = TEST_RESOURCES.resolve("test-resources").resolve("valid-single-file-project"); ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {validBalProject.resolve("main.bal").toString()}; new CommandLine(scanCmd).parseArgs(args); @@ -148,7 +148,7 @@ void testScanCommandSingleFileProject() throws IOException { @Test(description = "test scan command with single file project with project directory as argument") void testScanCommandSingleFileProjectWithDirectoryAsArgument() throws IOException { - Path parentDirectory = testResources.resolve("test-resources").toAbsolutePath(); + Path parentDirectory = TEST_RESOURCES.resolve("test-resources").toAbsolutePath(); ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {parentDirectory.resolve("valid-single-file-project").toString()}; new CommandLine(scanCmd).parseArgs(args); @@ -160,7 +160,7 @@ void testScanCommandSingleFileProjectWithDirectoryAsArgument() throws IOExceptio @Test(description = "test scan command with single file project without arguments") void testScanCommandSingleFileProjectWithoutArgument() throws IOException { - Path validBalProject = testResources.resolve("test-resources").resolve("valid-single-file-project"); + Path validBalProject = TEST_RESOURCES.resolve("test-resources").resolve("valid-single-file-project"); ScanCmd scanCmd = new ScanCmd(printStream); System.setProperty("user.dir", validBalProject.toString()); scanCmd.execute(); @@ -172,7 +172,7 @@ void testScanCommandSingleFileProjectWithoutArgument() throws IOException { @Test(description = "test scan command with single file project with too many arguments") void testScanCommandSingleFileProjectWithTooManyArguments() throws IOException { - Path validBalProject = testResources.resolve("test-resources").resolve("valid-single-file-project"); + Path validBalProject = TEST_RESOURCES.resolve("test-resources").resolve("valid-single-file-project"); ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"main.bal", "argument2"}; new CommandLine(scanCmd).parseArgs(args); @@ -226,7 +226,7 @@ void testScanCommandGenerateScanReportMethodWhenIssuePresent() throws IOExceptio @Test(description = "test method for printing static code analysis rules to the console") void testPrintRulesToConsole() throws IOException { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -248,7 +248,7 @@ void testScanCommandWithListRulesFlag() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"--list-rules"}; new CommandLine(scanCmd).parseArgs(args); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-config-file"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -260,7 +260,7 @@ void testScanCommandWithListRulesFlag() throws IOException { @Test(description = "test scan command with target directory flag on single file project") void testScanCommandWithTargetDirFlagOnSingleFileProject() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); - Path singleFileProject = testResources.resolve("test-resources") + Path singleFileProject = TEST_RESOURCES.resolve("test-resources") .resolve("valid-single-file-project").resolve("main.bal"); String[] args = {singleFileProject.toString(), "--target-dir=results"}; new CommandLine(scanCmd).parseArgs(args); @@ -272,7 +272,7 @@ void testScanCommandWithTargetDirFlagOnSingleFileProject() throws IOException { @Test(description = "test scan command with scan report flag on single file project") void testScanCommandWithScanReportFlagOnSingleFileProject() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); - Path singleFileProject = testResources.resolve("test-resources") + Path singleFileProject = TEST_RESOURCES.resolve("test-resources") .resolve("valid-single-file-project").resolve("main.bal"); String[] args = {singleFileProject.toString(), "--scan-report"}; new CommandLine(scanCmd).parseArgs(args); @@ -338,7 +338,7 @@ void testScanCommandWithIncludeRulesFlag() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"--include-rules=ballerina:1"}; new CommandLine(scanCmd).parseArgs(args); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-analyzer-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -355,7 +355,7 @@ void testScanCommandWithExcludeRulesFlag() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"--exclude-rules=ballerina:1"}; new CommandLine(scanCmd).parseArgs(args); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-analyzer-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -372,7 +372,7 @@ void testScanCommandWithIncludeAndExcludeRulesFlags() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"--include-rules=ballerina:1", "--exclude-rules=ballerina:1"}; new CommandLine(scanCmd).parseArgs(args); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-analyzer-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -384,7 +384,7 @@ void testScanCommandWithIncludeAndExcludeRulesFlags() throws IOException { @Test(description = "test scan command with include rules Scan.toml configurations") void testScanCommandWithIncludeRulesScanTomlConfigurations() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-include-rule-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -401,7 +401,7 @@ void testScanCommandWithExcludeRulesScanTomlConfigurations() throws IOException ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"--exclude-rules=ballerina:1"}; new CommandLine(scanCmd).parseArgs(args); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-exclude-rule-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -418,7 +418,7 @@ void testScanCommandWithIncludeAndExcludeRulesScanTomlConfigurations() throws IO ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"--include-rules=ballerina:1", "--exclude-rules=ballerina:1"}; new CommandLine(scanCmd).parseArgs(args); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-include-exclude-rule-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); scanCmd.execute(); @@ -430,7 +430,7 @@ void testScanCommandWithIncludeAndExcludeRulesScanTomlConfigurations() throws IO @Test(description = "test scan command with platform plugin configurations") void testScanCommandWithPlatformPluginConfigurations() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-platform-configurations"); Path rootProject = Path.of(System.getProperty("user.dir")).getParent(); Assert.assertNotNull(rootProject); @@ -441,7 +441,7 @@ void testScanCommandWithPlatformPluginConfigurations() throws IOException { .resolve("libs") .resolve("exampleOrg-static-code-analysis-platform-plugin.jar"); Assert.assertNotNull(platformPluginPath); - String tomlConfigurations = Files.readString(testResources.resolve("test-resources") + String tomlConfigurations = Files.readString(TEST_RESOURCES.resolve("test-resources") .resolve("platform-plugin-configurations.txt")); tomlConfigurations = tomlConfigurations.replace("__platform_name__", "examplePlatform"); tomlConfigurations = tomlConfigurations.replace("__platform_plugin_path__", @@ -472,7 +472,7 @@ void testScanCommandWithPlatformPluginConfigurations() throws IOException { @Test(description = "test scan command with invalid platform plugin configurations") void testScanCommandWithInvalidPlatformPluginConfigurations() throws IOException { ScanCmd scanCmd = new ScanCmd(printStream); - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-platform-configurations"); Path rootProject = Path.of(System.getProperty("user.dir")).getParent(); Assert.assertNotNull(rootProject); @@ -482,7 +482,7 @@ void testScanCommandWithInvalidPlatformPluginConfigurations() throws IOException .resolve("build") .resolve("libs") .resolve("exampleOrg-static-code-analysis-platform-plugin.jar"); - String tomlConfigurations = Files.readString(testResources.resolve("test-resources") + String tomlConfigurations = Files.readString(TEST_RESOURCES.resolve("test-resources") .resolve("platform-plugin-configurations.txt")); tomlConfigurations = tomlConfigurations.replace("__platform_name__", "invalidExamplePlatform"); tomlConfigurations = tomlConfigurations.replace("__platform_plugin_path__", diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/StaticCodeAnalyzerTest.java b/scan-command/src/test/java/io/ballerina/scan/internal/StaticCodeAnalyzerTest.java index 32faf327..3a5dbbdb 100644 --- a/scan-command/src/test/java/io/ballerina/scan/internal/StaticCodeAnalyzerTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/internal/StaticCodeAnalyzerTest.java @@ -31,6 +31,7 @@ import org.testng.Assert; import java.nio.file.Path; +import java.util.List; /** * Static code analyzer test. @@ -38,16 +39,33 @@ * @since 0.1.0 */ public class StaticCodeAnalyzerTest extends BaseTest { - private final Path coreRuleBalFiles = testResources.resolve("test-resources").resolve("core-rules"); + private static final Path CORE_RULE_BAL_FILES = TEST_RESOURCES.resolve("test-resources").resolve("core-rules"); - Document loadDocument(String documentName) { - Project project = SingleFileProject.load(coreRuleBalFiles.resolve(documentName)); - Module defaultModule = project.currentPackage().getDefaultModule(); + static List analyze(String documentName, List rules) { + Module defaultModule = getModule(documentName); + Document document = defaultModule.document(defaultModule.documentIds().iterator().next()); + ScannerContextImpl scannerContext = new ScannerContextImpl(rules); + SensitiveParameterTracker sensitiveParameterTracker = new SensitiveParameterTracker(document.syntaxTree(), + scannerContext, defaultModule.getCompilation().getSemanticModel()); + sensitiveParameterTracker.scan(); + StaticCodeAnalyzer staticCodeAnalyzer = new StaticCodeAnalyzer(document, + scannerContext, document.module().getCompilation().getSemanticModel()); + staticCodeAnalyzer.analyze(); + return scannerContext.getReporter().getIssues(); + } + + static Document loadDocument(String documentName) { + Module defaultModule = getModule(documentName); return defaultModule.document(defaultModule.documentIds().iterator().next()); } - void assertIssue(Issue issue, String documentName, int startLine, int startOffset, int endLine, int endOffset, - String ruleId, int numericId, String description, RuleKind ruleKind) { + private static Module getModule(String documentName) { + Project project = SingleFileProject.load(CORE_RULE_BAL_FILES.resolve(documentName)); + return project.currentPackage().getDefaultModule(); + } + + static void assertIssue(Issue issue, String documentName, int startLine, int startOffset, int endLine, + int endOffset, String ruleId, int numericId, String description, RuleKind ruleKind) { Assert.assertEquals(issue.source(), Source.BUILT_IN); LineRange location = issue.location().lineRange(); Assert.assertEquals(location.fileName(), documentName); @@ -61,4 +79,11 @@ void assertIssue(Issue issue, String documentName, int startLine, int startOffse Assert.assertEquals(rule.description(), description); Assert.assertEquals(rule.kind(), ruleKind); } + + static void assertIssue(Issue issue, String documentName, Location expectedLocation, Rule expectedRule) { + Assert.assertEquals(issue.source(), Source.BUILT_IN); + assertIssue(issue, documentName, expectedLocation.statLine(), expectedLocation.startOffset(), + expectedLocation.endLine(), expectedLocation.endOffset(), expectedRule.id(), + expectedRule.numericId(), expectedRule.description(), expectedRule.kind()); + } } diff --git a/scan-command/src/test/java/io/ballerina/scan/utils/ScanUtilsTest.java b/scan-command/src/test/java/io/ballerina/scan/utils/ScanUtilsTest.java index a55184b0..4b3ea195 100644 --- a/scan-command/src/test/java/io/ballerina/scan/utils/ScanUtilsTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/utils/ScanUtilsTest.java @@ -47,7 +47,7 @@ * @since 0.1.0 */ public class ScanUtilsTest extends BaseTest { - private final Path validBalProject = testResources.resolve("test-resources") + private final Path validBalProject = TEST_RESOURCES.resolve("test-resources") .resolve("valid-bal-project"); private static final String RESULTS_DIRECTORY = "results"; @@ -115,7 +115,7 @@ void testGenerateScanReportToProvidedDirectory() throws IOException { @Test(description = "test method for loading configurations from a Scan.toml file in a Ballerina single file project") void testloadScanTomlConfigurationsForSingleFileProject() { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("single-file-project-with-config-file").resolve("main.bal"); Project project = SingleFileProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -126,7 +126,7 @@ void testloadScanTomlConfigurationsForSingleFileProject() { @Test(description = "test method for loading configurations from a Scan.toml file") void testloadScanTomlConfigurations() { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -165,7 +165,7 @@ void testloadScanTomlConfigurations() { @Test(description = "test method for loading configurations from a Scan.toml file with invalid platform JAR") void testloadInvalidPlatformJAR() throws IOException { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-platform-jar"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -179,7 +179,7 @@ void testloadInvalidPlatformJAR() throws IOException { @Test(description = "test method for loading configurations from a Scan.toml file with invalid platform configuration") void testloadInvalidPlatformScanTomlConfigurations() throws IOException { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-platform-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -193,7 +193,7 @@ void testloadInvalidPlatformScanTomlConfigurations() throws IOException { @Test(description = "test method for loading configurations from a Scan.toml file with invalid Ballerina.toml configuration") void testloadScanTomlConfigurationsWithInvalidBallerinaTomlConfiguration() throws IOException { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-file-configuration"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -206,7 +206,7 @@ void testloadScanTomlConfigurationsWithInvalidBallerinaTomlConfiguration() throw @Test(description = "test method for loading configurations from an external configuration file") void testloadExternalScanTomlConfigurations() { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-external-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -244,7 +244,7 @@ void testloadExternalScanTomlConfigurations() { @Test(description = "test method for loading configurations from an invalid external configuration file") void testloadInvalidExternalScanTomlConfigurations() throws IOException { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-external-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -257,7 +257,7 @@ void testloadInvalidExternalScanTomlConfigurations() throws IOException { @Test(description = "test method for loading configurations from a remote configuration file") void testloadRemoteScanTomlConfigurations() { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-remote-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); @@ -295,7 +295,7 @@ void testloadRemoteScanTomlConfigurations() { @Test(description = "test method for loading configurations from an invalid remote configuration file") void testloadInvalidRemoteScanTomlConfigurations() throws IOException { - Path ballerinaProject = testResources.resolve("test-resources") + Path ballerinaProject = TEST_RESOURCES.resolve("test-resources") .resolve("bal-project-with-invalid-remote-config-file"); Project project = BuildProject.load(ballerinaProject); System.setProperty("user.dir", ballerinaProject.toString()); diff --git a/scan-command/src/test/resources/command-outputs/unix/list-rules-output.txt b/scan-command/src/test/resources/command-outputs/unix/list-rules-output.txt index 5ad2e669..7c12bd60 100644 --- a/scan-command/src/test/resources/command-outputs/unix/list-rules-output.txt +++ b/scan-command/src/test/resources/command-outputs/unix/list-rules-output.txt @@ -1,8 +1,10 @@ Loading scan tool configurations from src/test/resources/test-resources/bal-project-with-config-file/Scan.toml - RuleID | Rule Kind | Rule Description - --------------------------------------------------------------------------------------------- + RuleID | Rule Kind | Rule Description + ------------------------------------------------------------------------------------------------------------------- ballerina:1 | CODE_SMELL | Avoid checkpanic ballerina:2 | CODE_SMELL | Unused function parameter + ballerina:3 | VULNERABILITY | Hard-coded secrets are security-sensitive + ballerina:4 | VULNERABILITY | Non configurable secrets are security-sensitive ballerina/example_module_static_code_analyzer:1 | CODE_SMELL | rule 1 ballerina/example_module_static_code_analyzer:2 | BUG | rule 2 ballerina/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 @@ -11,4 +13,4 @@ Loading scan tool configurations from src/test/resources/test-resources/bal-proj ballerinax/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 exampleOrg/example_module_static_code_analyzer:1 | CODE_SMELL | rule 1 exampleOrg/example_module_static_code_analyzer:2 | BUG | rule 2 - exampleOrg/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 + exampleOrg/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 \ No newline at end of file diff --git a/scan-command/src/test/resources/command-outputs/unix/print-rules-to-console.txt b/scan-command/src/test/resources/command-outputs/unix/print-rules-to-console.txt index 431cad6f..7c12bd60 100644 --- a/scan-command/src/test/resources/command-outputs/unix/print-rules-to-console.txt +++ b/scan-command/src/test/resources/command-outputs/unix/print-rules-to-console.txt @@ -1,8 +1,10 @@ Loading scan tool configurations from src/test/resources/test-resources/bal-project-with-config-file/Scan.toml - RuleID | Rule Kind | Rule Description - --------------------------------------------------------------------------------------------- + RuleID | Rule Kind | Rule Description + ------------------------------------------------------------------------------------------------------------------- ballerina:1 | CODE_SMELL | Avoid checkpanic ballerina:2 | CODE_SMELL | Unused function parameter + ballerina:3 | VULNERABILITY | Hard-coded secrets are security-sensitive + ballerina:4 | VULNERABILITY | Non configurable secrets are security-sensitive ballerina/example_module_static_code_analyzer:1 | CODE_SMELL | rule 1 ballerina/example_module_static_code_analyzer:2 | BUG | rule 2 ballerina/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 diff --git a/scan-command/src/test/resources/command-outputs/windows/list-rules-output.txt b/scan-command/src/test/resources/command-outputs/windows/list-rules-output.txt index e384b4ea..f72b25be 100644 --- a/scan-command/src/test/resources/command-outputs/windows/list-rules-output.txt +++ b/scan-command/src/test/resources/command-outputs/windows/list-rules-output.txt @@ -1,8 +1,10 @@ Loading scan tool configurations from src\test\resources\test-resources\bal-project-with-config-file\Scan.toml - RuleID | Rule Kind | Rule Description - --------------------------------------------------------------------------------------------- + RuleID | Rule Kind | Rule Description + ------------------------------------------------------------------------------------------------------------------- ballerina:1 | CODE_SMELL | Avoid checkpanic ballerina:2 | CODE_SMELL | Unused function parameter + ballerina:3 | VULNERABILITY | Hard-coded secrets are security-sensitive + ballerina:4 | VULNERABILITY | Non configurable secrets are security-sensitive ballerina/example_module_static_code_analyzer:1 | CODE_SMELL | rule 1 ballerina/example_module_static_code_analyzer:2 | BUG | rule 2 ballerina/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 @@ -11,4 +13,4 @@ Loading scan tool configurations from src\test\resources\test-resources\bal-proj ballerinax/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 exampleOrg/example_module_static_code_analyzer:1 | CODE_SMELL | rule 1 exampleOrg/example_module_static_code_analyzer:2 | BUG | rule 2 - exampleOrg/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 + exampleOrg/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 \ No newline at end of file diff --git a/scan-command/src/test/resources/command-outputs/windows/print-rules-to-console.txt b/scan-command/src/test/resources/command-outputs/windows/print-rules-to-console.txt index cab063a4..f72b25be 100644 --- a/scan-command/src/test/resources/command-outputs/windows/print-rules-to-console.txt +++ b/scan-command/src/test/resources/command-outputs/windows/print-rules-to-console.txt @@ -1,8 +1,10 @@ Loading scan tool configurations from src\test\resources\test-resources\bal-project-with-config-file\Scan.toml - RuleID | Rule Kind | Rule Description - --------------------------------------------------------------------------------------------- + RuleID | Rule Kind | Rule Description + ------------------------------------------------------------------------------------------------------------------- ballerina:1 | CODE_SMELL | Avoid checkpanic ballerina:2 | CODE_SMELL | Unused function parameter + ballerina:3 | VULNERABILITY | Hard-coded secrets are security-sensitive + ballerina:4 | VULNERABILITY | Non configurable secrets are security-sensitive ballerina/example_module_static_code_analyzer:1 | CODE_SMELL | rule 1 ballerina/example_module_static_code_analyzer:2 | BUG | rule 2 ballerina/example_module_static_code_analyzer:3 | VULNERABILITY | rule 3 diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_constant_assignment.bal b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_constant_assignment.bal new file mode 100644 index 00000000..66ec7637 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_constant_assignment.bal @@ -0,0 +1,68 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +const DEFAULT_VALUE = "secret"; +const URL_WITH_CREDENTIAL = "http://username:securepass@localhost"; + +public type Credential record { + string username; + string password = DEFAULT_VALUE; +}; + +public type Config record { + record {|Credential credential;|} nested; +}; + +public function main() { + printConfig(nested = {credential: {username: "sabthar", password: DEFAULT_VALUE}}); + PasswordStrengthChecker checker = new; + checker.checkStregth({username: "sabthar", password: DEFAULT_VALUE}); + Client 'client = new (config = {nested: {credential: {username: "sabthar", password: DEFAULT_VALUE}}}); + json clientConfig = {clientId: "xyz", clientSecret: DEFAULT_VALUE}; +} + +function printConfig(*Config config) { + io:println(config); +} + +class PasswordStrengthChecker { + function checkStregth(Credential credential) { + io:println(string `Username: ${credential.username}`, + string ` Password Strength: ${passwordStrength(credential.password)}%`); + } +} + +function passwordStrength(string word) returns float { + return word.length() / 12 * 100; +} + +class Client { + isolated function init(Config config) { + io:println(config); + } +} + +function printUserInfo(string username, string password = DEFAULT_VALUE) { + io:println(string `Username: ${username}, Password strength: ${passwordStrength(password)}%`); +} + +class PasswordStrenghtChecker { + function strength(string username, string secret = DEFAULT_VALUE) { + io:println(string `Username: ${username}, Password strength: ${passwordStrength(secret)}%`); + } +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_default_record_field_value.bal b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_default_record_field_value.bal new file mode 100644 index 00000000..3eb279e5 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_default_record_field_value.bal @@ -0,0 +1,25 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +public type Credential record { + string username; + string password = "password"; +}; + +public type ClientCredential record { + string clientId; + string clientSecret = "secret"; +}; diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_local_secret_variable.bal b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_local_secret_variable.bal new file mode 100644 index 00000000..70632d3f --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_local_secret_variable.bal @@ -0,0 +1,24 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +public function main() { + string password = "password"; + string passwd = "passwd"; + string pwd = "pwd"; + string passphrase = "passphrase"; + string secret = "secret"; + string clientSecret = "clientSecret"; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_module_level_secret_variable.bal b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_module_level_secret_variable.bal new file mode 100644 index 00000000..ece25850 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_module_level_secret_variable.bal @@ -0,0 +1,22 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +string password = "password"; +string passwd = "passwd"; +string pwd = "pwd"; +string passphrase = "passphrase"; +string secret = "secret"; +string clientSecret = "clientSecret"; diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_secret_constant.bal b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_secret_constant.bal new file mode 100644 index 00000000..8ae92364 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_secret_constant.bal @@ -0,0 +1,18 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +const PASSWORD = "password"; +const API_KEY = "234567890234567890-34567890-dkajf"; diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_secret_record_field_in_argument.bal b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_secret_record_field_in_argument.bal new file mode 100644 index 00000000..5fb4c983 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule003_hardcoded_secret_record_field_in_argument.bal @@ -0,0 +1,54 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +public type Credential record { + string username; + string password; +}; + +public type Config record { + record {|Credential credential;|} nested; +}; + +public function main() { + printConfig(nested = {credential: {username: "sabthar", password: "secret"}}); + PasswordStrengthChecker checker = new; + checker.checkStregth({username: "sabthar", password: "secret"}); + Client 'client = new (config = {nested: {credential: {username: "sabthar", password: "pwd"}}}); +} + +function printConfig(*Config config) { + io:println(config); +} + +class PasswordStrengthChecker { + function checkStregth(Credential credential) { + io:println(string `Username: ${credential.username}`, + string ` Password Strength: ${passwordStrength(credential.password)}%`); + } +} + +function passwordStrength(string word) returns float { + return word.length() / 12 * 100; +} + +class Client { + isolated function init(Config config) { + io:println(config); + } +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_default_record_field.bal b/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_default_record_field.bal new file mode 100644 index 00000000..baf079ed --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_default_record_field.bal @@ -0,0 +1,27 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +final string defaultValue = "password"; + +public type Credential record { + string username; + string password = defaultValue; +}; + +public type ClientCredential record {| + string clientId; + string clientSecret = defaultValue; +|}; diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_record_field_argument.bal b/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_record_field_argument.bal new file mode 100644 index 00000000..ba0f3949 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_record_field_argument.bal @@ -0,0 +1,56 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +string defaultValue = "secret"; + +public type Credential record { + string username; + string password; +}; + +public type Config record { + record {|Credential credential;|} nested; +}; + +public function main() { + printConfig(nested = {credential: {username: "sabthar", password: defaultValue}}); + PasswordStrengthChecker checker = new; + checker.checkStregth({username: "sabthar", password: defaultValue}); + Client 'client = new (config = {nested: {credential: {username: "sabthar", password: defaultValue}}}); +} + +function printConfig(*Config config) { + io:println(config); +} + +class PasswordStrengthChecker { + function checkStregth(Credential credential) { + io:println(string `Username: ${credential.username}`, + string ` Password Strength: ${passwordStrength(credential.password)}%`); + } +} + +function passwordStrength(string word) returns float { + return word.length() / 12 * 100; +} + +class Client { + isolated function init(Config config) { + io:println(config); + } +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_secret.bal b/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_secret.bal new file mode 100644 index 00000000..fad0f826 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rule004_non_config_variable_assignment_to_secret.bal @@ -0,0 +1,27 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +string defaultValue = ""; + +public function main() { + string password = defaultValue; + string passwd = defaultValue; + string pwd = defaultValue; + string passphrase = defaultValue; + string secret = defaultValue; + string clientSecret = defaultValue; +} + diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcode_secret_for_map_field.bal b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcode_secret_for_map_field.bal new file mode 100644 index 00000000..59dc68e6 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcode_secret_for_map_field.bal @@ -0,0 +1,32 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +public type Credential record { + string username; + string password; +}; + +public type Config record { + record {| + Credential credential; + |} nested; +}; + +public function main() { + Credential credential = {username: "sabthar", password: "password"}; + Config config = {nested: {credential: {username: "sabthar", password: "pwd"}}}; + json clientConfig = {clientId: "xyz", clientSecret: "password"}; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_default_secret_parameter.bal b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_default_secret_parameter.bal new file mode 100644 index 00000000..0e043948 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_default_secret_parameter.bal @@ -0,0 +1,31 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +function printUserInfo(string username, string password = "defaultPassword") { + io:println(string `Username: ${username}, Password strength: ${passwordStrength(password)}%`); +} + +class PasswordStrenghtChecker { + function strength(string username, string secret = "secret") { + io:println(string `Username: ${username}, Password strength: ${passwordStrength(secret)}%`); + } +} + +function passwordStrength(string word) returns float { + return word.length() / 12 * 100; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_secret_function_argument.bal b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_secret_function_argument.bal new file mode 100644 index 00000000..f399ca2a --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_secret_function_argument.bal @@ -0,0 +1,54 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +public function main() { + printPasswordStreanght("sabthar", "password"); + printPasswordStreanght("sabthar", password = "secret"); + + PasswordStrengthChecker checker = new (); + checker.checkStregth("sabthar", "secret"); + checker.checkStregth("sabthar", password = "secret"); + + Client 'client = new ("password", "secret"); // TODO: Find a way to capture this + 'client = new ("sabthar", password = "password"); +} + +function printPasswordStreanght(string username, string password) { + io:println(string `Username: ${username}, Password Strength: ${calculateStreanght(password)}%`); +} + +function calculateStreanght(string word) returns float { + return word.length() / 12 * 100; +} + +class PasswordStrengthChecker { + function checkStregth(string username, string password) { + io:println(string `Username: ${username}, Password Strength: ${calculateStreanght(password)}%`); + } + + function test() { + self.checkStregth("sabthar", "secret"); + self.checkStregth(username = "sabthar", password = "secret"); + } +} + +class Client { + function init(string username, string password) { + io:println(string `Username: ${username}, Password Strength: ${calculateStreanght(password)}%`); + } +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_secret_object_field.bal b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_secret_object_field.bal new file mode 100644 index 00000000..b14eaf51 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules003_hardcoded_secret_object_field.bal @@ -0,0 +1,20 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +class Client { + final string secret = "default secret"; + final string pwd = "default pwd"; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules003_url_with_hardcoded_credentials.bal b/scan-command/src/test/resources/test-resources/core-rules/rules003_url_with_hardcoded_credentials.bal new file mode 100644 index 00000000..c0c3753f --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules003_url_with_hardcoded_credentials.bal @@ -0,0 +1,22 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +string urlWithCredential = "http://username:securepass@localhost"; + +public function main() { + string loginPath = "login"; + string urlWithCredential = string `http://username:securepass@localhost/${loginPath}`; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_expression.bal b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_expression.bal new file mode 100644 index 00000000..2f54e725 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_expression.bal @@ -0,0 +1,22 @@ +// // Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// // +// // WSO2 Inc. 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. + +string a = "pass"; +string b = "word"; + +public function main() { + string password = a + b; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_secret_as_function_argument.bal b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_secret_as_function_argument.bal new file mode 100644 index 00000000..a81cba30 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_secret_as_function_argument.bal @@ -0,0 +1,56 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +string defaultValue = "password"; + +public function main() { + printPasswordStreanght("sabthar", defaultValue); + printPasswordStreanght("sabthar", password = defaultValue); + + PasswordStrengthChecker checker = new (); + checker.checkStregth("sabthar", defaultValue); // Can't get expression symbol for defaultValue + checker.checkStregth("sabthar", password = defaultValue); + + Client 'client = new ("password", defaultValue); // TODO: Find a way to capture this + 'client = new ("sabthar", password = defaultValue); +} + +function printPasswordStreanght(string username, string password) { + io:println(string `Username: ${username}, Password Strength: ${calculateStreanght(password)}%`); +} + +function calculateStreanght(string word) returns float { + return word.length() / 12 * 100; +} + +class PasswordStrengthChecker { + function checkStregth(string username, string password) { + io:println(string `Username: ${username}, Password Strength: ${calculateStreanght(password)}%`); + } + + function test() { + self.checkStregth("sabthar", defaultValue); + self.checkStregth(username = "sabthar", password = defaultValue); + } +} + +class Client { + function init(string username, string password) { + io:println(string `Username: ${username}, Password Strength: ${calculateStreanght(password)}%`); + } +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_secret_in_map_field.bal b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_secret_in_map_field.bal new file mode 100644 index 00000000..a2093465 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_secret_in_map_field.bal @@ -0,0 +1,34 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +string defaultValue = "password"; + +public type Credential record { + string username; + string password; +}; + +public type Config record { + record {| + Credential credential; + |} nested; +}; + +public function main() { + Credential credential = {username: "sabthar", password: defaultValue}; + Config config = {nested: {credential: {username: "sabthar", password: defaultValue}}}; + json clientConfig = {clientId: "xyz", clientSecret: defaultValue}; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_value_in_short_hand_field.bal b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_value_in_short_hand_field.bal new file mode 100644 index 00000000..8eced7d8 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_value_in_short_hand_field.bal @@ -0,0 +1,26 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +string password = "password"; + +public type Credential record { + string username; + string password; +}; + +public function main() { + Credential credential = {username: "sabthar", password}; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_variable_assignment_to_default_parameter.bal b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_variable_assignment_to_default_parameter.bal new file mode 100644 index 00000000..9abd1081 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_variable_assignment_to_default_parameter.bal @@ -0,0 +1,33 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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/io; + +string defaultValue = "password"; + +function printUserInfo(string username, string password = defaultValue) { + io:println(string `Username: ${username}, Password strength: ${passwordStrength(password)}%`); +} + +class PasswordStrenghtChecker { + function strength(string username, string secret = defaultValue) { + io:println(string `Username: ${username}, Password strength: ${passwordStrength(secret)}%`); + } +} + +function passwordStrength(string word) returns float { + return word.length() / 12 * 100; +} diff --git a/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_variable_assignment_to_object_field.bal b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_variable_assignment_to_object_field.bal new file mode 100644 index 00000000..809076e8 --- /dev/null +++ b/scan-command/src/test/resources/test-resources/core-rules/rules004_non_config_variable_assignment_to_object_field.bal @@ -0,0 +1,22 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 Inc. 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. + +final string defaultValue = "default secret"; + +class Client { + final string secret = defaultValue; + final string password = defaultValue; +} diff --git a/scan-command/src/test/resources/testng.xml b/scan-command/src/test/resources/testng.xml index cbdaee06..9cc39786 100644 --- a/scan-command/src/test/resources/testng.xml +++ b/scan-command/src/test/resources/testng.xml @@ -31,6 +31,8 @@ under the License. + + diff --git a/scan-command/tool-scan/BalTool.toml b/scan-command/tool-scan/BalTool.toml index f5e6bc37..d623d7ad 100644 --- a/scan-command/tool-scan/BalTool.toml +++ b/scan-command/tool-scan/BalTool.toml @@ -2,4 +2,4 @@ id = "scan" [[dependency]] -path = "../build/libs/scan-command-0.5.0.jar" +path = "../build/libs/scan-command-0.5.1.jar" diff --git a/scan-command/tool-scan/Ballerina.toml b/scan-command/tool-scan/Ballerina.toml index a45389a3..534e75c2 100644 --- a/scan-command/tool-scan/Ballerina.toml +++ b/scan-command/tool-scan/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "tool_scan" -version = "0.5.0" +version = "0.5.1" distribution = "2201.10.2" authors = ["Ballerina"] keywords = ["scan", "static code analysis"] diff --git a/scan-command/tool-scan/Dependencies.toml b/scan-command/tool-scan/Dependencies.toml index d68ea6ba..a009516e 100644 --- a/scan-command/tool-scan/Dependencies.toml +++ b/scan-command/tool-scan/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.10.2" [[package]] org = "ballerina" name = "tool_scan" -version = "0.5.0" +version = "0.5.1" modules = [ {org = "ballerina", packageName = "tool_scan", moduleName = "tool_scan"} ]