All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.python.checks.hotspots.HardCodedCredentialsCheck Maven / Gradle / Ivy

There is a newer version: 4.23.0.17664
Show newest version
/*
 * SonarQube Python Plugin
 * Copyright (C) 2011-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.python.checks.hotspots;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.Argument;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
import org.sonar.plugins.python.api.tree.DictionaryLiteralElement;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.ExpressionList;
import org.sonar.plugins.python.api.tree.FileInput;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.KeyValuePair;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.Parameter;
import org.sonar.plugins.python.api.tree.ParameterList;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.StringElement;
import org.sonar.plugins.python.api.tree.StringLiteral;
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tree.Kind;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S2068")
public class HardCodedCredentialsCheck extends PythonSubscriptionCheck {

  private static final String DEFAULT_CREDENTIAL_WORDS = "password,passwd,pwd,passphrase";

  private static final String FLASK_CONFIG_ASSIGNMENT_FQN = "flask.app.Flask.config";
  private static final String FLASK_CONFIG_CREDENTIAL_KEY = "SECRET_KEY";

  @RuleProperty(
    key = "credentialWords",
    description = "Comma separated list of words identifying potential credentials",
    defaultValue = DEFAULT_CREDENTIAL_WORDS)
  public String credentialWords = DEFAULT_CREDENTIAL_WORDS;

  public static final String MESSAGE = "\"%s\" detected here, review this potentially hard-coded credential.";

  private List variablePatterns = null;
  private List literalPatterns = null;
  private Map sensitiveArgumentByFQN;

  private Map sensitiveArgumentByFQN() {
    if (sensitiveArgumentByFQN == null) {
      sensitiveArgumentByFQN = new HashMap<>();
      sensitiveArgumentByFQN.put("mysql.connector.connect", 2);
      sensitiveArgumentByFQN.put("mysql.connector.connection.MySQLConnection", 2);
      sensitiveArgumentByFQN.put("pymysql.connect", 2);
      sensitiveArgumentByFQN.put("pymysql.connections.connect", 2);
      sensitiveArgumentByFQN.put("pymysql.connections.Connection", 2);
      sensitiveArgumentByFQN.put("psycopg2.connect", 2);
      sensitiveArgumentByFQN.put("pgdb.connect", 2);
      sensitiveArgumentByFQN.put("pgdb.connect.connect", 2);
      sensitiveArgumentByFQN.put("pg.DB", 5);
      sensitiveArgumentByFQN.put("pg.connect", 5);
      sensitiveArgumentByFQN = Collections.unmodifiableMap(sensitiveArgumentByFQN);
    }
    return sensitiveArgumentByFQN;
  }

  private Stream variablePatterns() {
    if (variablePatterns == null) {
      variablePatterns = toPatterns("");
    }
    return variablePatterns.stream();
  }

  private Stream literalPatterns() {
    if (literalPatterns == null) {
      // Avoid raising on prepared statements
      String credentials = Stream.of(credentialWords.split(","))
        .map(String::trim).collect(Collectors.joining("|"));
      literalPatterns = toPatterns("=(?!.*(" + credentials + "))[^:%'?{\\s]+");
    }
    return literalPatterns.stream();
  }

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Kind.ASSIGNMENT_STMT, ctx -> handleAssignmentStatement((AssignmentStatement) ctx.syntaxNode(), ctx));
    context.registerSyntaxNodeConsumer(Kind.STRING_LITERAL, ctx -> handleStringLiteral((StringLiteral) ctx.syntaxNode(), ctx));
    context.registerSyntaxNodeConsumer(Kind.CALL_EXPR, ctx -> handleCallExpression((CallExpression) ctx.syntaxNode(), ctx));
    context.registerSyntaxNodeConsumer(Kind.REGULAR_ARGUMENT, ctx -> handleRegularArgument((RegularArgument) ctx.syntaxNode(), ctx));
    context.registerSyntaxNodeConsumer(Kind.PARAMETER_LIST, ctx -> handleParameterList((ParameterList) ctx.syntaxNode(), ctx));
    context.registerSyntaxNodeConsumer(Kind.DICTIONARY_LITERAL, ctx -> handleDictionaryLiteral((DictionaryLiteral) ctx.syntaxNode(), ctx));
  }

  private void handleDictionaryLiteral(DictionaryLiteral dictionaryLiteral, SubscriptionContext ctx) {
    for (DictionaryLiteralElement dictionaryLiteralElement : dictionaryLiteral.elements()) {
      if (dictionaryLiteralElement.is(Kind.KEY_VALUE_PAIR)) {
        KeyValuePair keyValuePair = (KeyValuePair) dictionaryLiteralElement;
        checkKeyValuePair(keyValuePair, ctx);
      }
    }
  }

  private void checkKeyValuePair(KeyValuePair keyValuePair, SubscriptionContext ctx) {
    if (keyValuePair.key().is(Kind.STRING_LITERAL) && keyValuePair.value().is(Kind.STRING_LITERAL)) {
      String matchedCredential = matchedCredential(((StringLiteral) keyValuePair.key()).trimmedQuotesValue(), variablePatterns());
      if (matchedCredential != null) {
        StringLiteral literal = (StringLiteral) keyValuePair.value();
        if (isSuspiciousStringLiteral(literal)) {
          ctx.addIssue(keyValuePair, String.format(MESSAGE, matchedCredential));
        }
      }
    }
  }

  private void handleParameterList(ParameterList parameterList, SubscriptionContext ctx) {
    for (Parameter parameter : parameterList.nonTuple()) {
      Name parameterName = parameter.name();
      if (parameterName == null) {
        continue;
      }
      Expression defaultValue = parameter.defaultValue();
      String matchedCredential = matchedCredential(parameterName.name(), variablePatterns());
      if (matchedCredential != null && defaultValue != null && isSuspiciousStringLiteral(defaultValue)) {
        ctx.addIssue(parameter, String.format(MESSAGE, matchedCredential));
      }
    }
  }

  private void handleRegularArgument(RegularArgument regularArgument, SubscriptionContext ctx) {
    Name keywordArgument = regularArgument.keywordArgument();
    if (keywordArgument != null) {
      String matchedCredential = matchedCredential(keywordArgument.name(), variablePatterns());
      if (matchedCredential != null && isSuspiciousStringLiteral(regularArgument.expression())) {
        ctx.addIssue(regularArgument, String.format(MESSAGE, matchedCredential));
      }
    }
  }

  private void handleCallExpression(CallExpression callExpression, SubscriptionContext ctx) {
    if (callExpression.arguments().isEmpty()) {
      return;
    }
    Symbol calleeSymbol = callExpression.calleeSymbol();
    if (calleeSymbol != null && sensitiveArgumentByFQN().containsKey(calleeSymbol.fullyQualifiedName())) {
      checkSensitiveArgument(callExpression, sensitiveArgumentByFQN().get(calleeSymbol.fullyQualifiedName()), ctx);
    }
  }

  private static void checkSensitiveArgument(CallExpression callExpression, int argNb, SubscriptionContext ctx) {
    for (int i = 0; i < callExpression.arguments().size(); i++) {
      Argument argument = callExpression.arguments().get(i);
      if (argument.is(Kind.REGULAR_ARGUMENT)) {
        RegularArgument regularArgument = (RegularArgument) argument;
        if (regularArgument.keywordArgument() != null) {
          return;
        } else if (i == argNb && regularArgument.expression().is(Kind.STRING_LITERAL) && !((StringLiteral) regularArgument.expression()).trimmedQuotesValue().isEmpty()) {
          ctx.addIssue(regularArgument, "Review this potentially hard-coded credential.");
        }
      }
    }
  }

  private void handleStringLiteral(StringLiteral stringLiteral, SubscriptionContext ctx) {
    if (isDocString(stringLiteral)) {
      return;
    }
    if (stringLiteral.stringElements().stream().anyMatch(StringElement::isInterpolated)) {
      return;
    }

    String literalValue = stringLiteral.trimmedQuotesValue();

    if (isFlaskConfigAssignment(stringLiteral)) {
      if (FLASK_CONFIG_CREDENTIAL_KEY.equals(literalValue)) {
        ctx.addIssue(stringLiteral, String.format(MESSAGE, FLASK_CONFIG_CREDENTIAL_KEY));
      }
      return;
    }

    String matchedCredential = matchedCredential(literalValue, literalPatterns());
    if (matchedCredential != null) {
      ctx.addIssue(stringLiteral, String.format(MESSAGE, matchedCredential));
    }
    if (isURLWithCredentials(stringLiteral)) {
      ctx.addIssue(stringLiteral, "Review this hard-coded URL, which may contain a credential.");
    }
  }

  /**
   * Flask config assignment should not raise an issue except an assignment to `SECRET_KEY`
   * See SONARPY-1061 for more context
   */
  private static boolean isFlaskConfigAssignment(Expression expression) {
    if (expression.parent().is(Kind.ASSIGNMENT_STMT)) {
      AssignmentStatement assignment = (AssignmentStatement) expression.parent();
      return assignment.lhsExpressions().stream()
        .map(ExpressionList::expressions)
        .flatMap(Collection::stream)
        .anyMatch(HardCodedCredentialsCheck::isFlaskConfigSubscription);
    }
    return Optional.ofNullable(TreeUtils.firstAncestorOfKind(expression, Kind.SUBSCRIPTION))
      .filter(HardCodedCredentialsCheck::isFlaskConfigSubscription)
      .isPresent();
  }

  private static boolean isFlaskConfigSubscription(Tree tree) {
    if (tree.is(Kind.SUBSCRIPTION)) {
      return getSubscriptionFqn((SubscriptionExpression) tree)
        .filter(FLASK_CONFIG_ASSIGNMENT_FQN::equals)
        .isPresent();
    }
    return false;
  }

  private static Optional getSubscriptionFqn(SubscriptionExpression subscription) {
    return Optional.of(subscription.object())
      .filter(QualifiedExpression.class::isInstance)
      .map(qualifier -> ((QualifiedExpression) qualifier).name().symbol())
      .map(Symbol::fullyQualifiedName);
  }

  private static boolean isDocString(StringLiteral stringLiteral) {
    Tree parent = TreeUtils.firstAncestorOfKind(stringLiteral, Kind.FILE_INPUT, Kind.CLASSDEF, Kind.FUNCDEF);
    return Optional.ofNullable(parent)
      .map(p -> ((p.is(Kind.FILE_INPUT) && stringLiteral.equals(((FileInput) p).docstring()))
        || (p.is(Kind.CLASSDEF) && stringLiteral.equals(((ClassDef) p).docstring()))
        || (p.is(Kind.FUNCDEF) && stringLiteral.equals(((FunctionDef) p).docstring()))))
      .orElse(false);
  }

  private static boolean isURLWithCredentials(StringLiteral stringLiteral) {
    if (!stringLiteral.trimmedQuotesValue().contains("://")) {
      return false;
    }
    try {
      URL url = new URL(stringLiteral.trimmedQuotesValue());
      String userInfo = url.getUserInfo();
      if (userInfo != null && userInfo.matches("\\S+:\\S+")) {
        return true;
      }
    } catch (MalformedURLException e) {
      return false;
    }
    return false;
  }

  private void handleAssignmentStatement(AssignmentStatement assignmentStatement, SubscriptionContext ctx) {
    ExpressionList lhs = assignmentStatement.lhsExpressions().get(0);
    Expression expression = lhs.expressions().get(0);

    if (expression instanceof HasSymbol hasSymbol) {
      Symbol symbol = hasSymbol.symbol();
      String matchedCredential = credentialSymbolName(symbol);
      if (matchedCredential != null) {
        checkAssignedValue(assignmentStatement, matchedCredential, ctx);
      }
    }

    if (expression.is(Kind.SUBSCRIPTION)) {
      SubscriptionExpression subscriptionExpression = (SubscriptionExpression) expression;
      for (Expression expr : subscriptionExpression.subscripts().expressions()) {
        if (expr.is(Kind.STRING_LITERAL)) {
          String matchedCredential = matchedCredential(((StringLiteral) expr).trimmedQuotesValue(), variablePatterns());
          if (matchedCredential != null) {
            checkAssignedValue(assignmentStatement, matchedCredential, ctx);
          }
        }
      }
    }
  }

  private void checkAssignedValue(AssignmentStatement assignmentStatement, String matchedCredential, SubscriptionContext ctx) {
    Expression assignedValue = assignmentStatement.assignedValue();
    if (isSuspiciousStringLiteral(assignedValue) && !isFlaskConfigAssignment(assignedValue) ) {
      ctx.addIssue(assignmentStatement, String.format(MESSAGE, matchedCredential));
    }
  }

  private String credentialSymbolName(@CheckForNull Symbol symbol) {
    if (symbol != null) {
      return matchedCredential(symbol.name(), variablePatterns());
    }
    return null;
  }

  private boolean isSuspiciousStringLiteral(Tree tree) {
    return tree.is(Kind.STRING_LITERAL) && !((StringLiteral) tree).trimmedQuotesValue().isEmpty()
      && !isCredential(((StringLiteral) tree).trimmedQuotesValue(), variablePatterns());
  }

  private static boolean isCredential(String target, Stream patterns) {
    return patterns.anyMatch(pattern -> pattern.matcher(target).find());
  }

  private static String matchedCredential(String target, Stream patterns) {
    Optional matched = patterns.filter(pattern -> pattern.matcher(target).find()).findFirst();
    if (matched.isPresent()) {
      String matchedPattern = matched.get().pattern();
      int suffixStart = matchedPattern.indexOf('=');
      if (suffixStart > 0) {
        return matchedPattern.substring(0, suffixStart);
      } else {
        return matchedPattern;
      }
    }
    return null;
  }

  private List toPatterns(String suffix) {
    return Stream.of(credentialWords.split(","))
      .map(String::trim)
      .map(word -> Pattern.compile(word + suffix, Pattern.CASE_INSENSITIVE))
      .toList();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy