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

org.sonar.python.checks.FlaskHardCodedSecret Maven / Gradle / Ivy

The 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;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
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.AssignmentStatement;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.ExpressionList;
import org.sonar.plugins.python.api.tree.KeyValuePair;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
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.python.checks.utils.Expressions;
import org.sonar.python.tree.TreeUtils;


public abstract class FlaskHardCodedSecret extends PythonSubscriptionCheck {
  private static final String MESSAGE = "Don't disclose %s secret keys.";
  private static final String SECONDARY_MESSAGE = "Assignment to sensitive property.";
  private static final Set FLASK_APP_CONFIG_QUALIFIER_FQNS = Set.of(
    "flask.app.Flask.config",
    "flask.globals.current_app.config"
  );
  public static final String SECONDARY_LOCATION_MESSAGE = "The secret is used in this call.";

  protected abstract String getSecretKeyKeyword();

  protected abstract String getSecretKeyType();

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::verifyCallExpression);
    context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, this::verifyAssignmentStatement);
  }

  private void verifyCallExpression(SubscriptionContext ctx) {
    CallExpression callExpression = (CallExpression) ctx.syntaxNode();
    Optional.of(callExpression)
      .map(CallExpression::callee)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
      .filter(qualiExpr -> "update".equals(qualiExpr.name().name()))
      .map(QualifiedExpression::qualifier)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
      .map(QualifiedExpression::name)
      .map(Name::symbol)
      .map(Symbol::fullyQualifiedName)
      .filter(FLASK_APP_CONFIG_QUALIFIER_FQNS::contains)
      .ifPresent(fqn -> verifyUpdateCallArgument(ctx, callExpression));
  }

  private void verifyUpdateCallArgument(SubscriptionContext ctx, CallExpression callExpression) {
    Optional.of(callExpression.arguments())
      .filter(arguments -> arguments.size() == 1)
      .map(arguments -> arguments.get(0))
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(RegularArgument.class))
      .map(RegularArgument::expression)
      .map(FlaskHardCodedSecret::getAssignedValue)
      .flatMap(this::getIllegalDictArgument)
      .ifPresent(illegalArgument -> ctx.addIssue(illegalArgument, getMessage())
        .secondary(callExpression.callee(), SECONDARY_LOCATION_MESSAGE));
  }

  private String getMessage() {
    return String.format(MESSAGE, getSecretKeyType());
  }

  private static Expression getAssignedValue(Expression expression) {
    if (expression.is(Tree.Kind.NAME)) {
      return Expressions.singleAssignedValue((Name) expression);
    }
    return expression;
  }

  private Optional getIllegalDictArgument(Expression expression) {
    if (expression.is(Tree.Kind.CALL_EXPR)) {
      return TreeUtils.toOptionalInstanceOf(CallExpression.class, expression)
        .filter(FlaskHardCodedSecret::isCallToDictConstructor)
        .flatMap(this::getIllegalKeywordArgument);
    } else if (expression.is(Tree.Kind.DICTIONARY_LITERAL)) {
      return TreeUtils.toOptionalInstanceOf(DictionaryLiteral.class, expression)
        .flatMap(this::getIllegalKeyValuePair);
    }
    return Optional.empty();
  }

  private static boolean isCallToDictConstructor(CallExpression callExpression) {
    return Optional.of(callExpression)
      .map(CallExpression::callee)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(Name.class))
      .map(Name::symbol)
      .map(Symbol::fullyQualifiedName)
      .filter("dict"::equals)
      .isPresent();
  }

  private Optional getIllegalKeyValuePair(DictionaryLiteral dictionaryLiteral) {
    return dictionaryLiteral.elements().stream()
      .filter(KeyValuePair.class::isInstance)
      .map(KeyValuePair.class::cast)
      .filter(this::isIllegalKeyValuePair)
      .findFirst();
  }

  private boolean isIllegalKeyValuePair(KeyValuePair keyValuePair) {
    return Optional.of(keyValuePair.key())
      .filter(StringLiteral.class::isInstance)
      .map(StringLiteral.class::cast)
      .map(StringLiteral::trimmedQuotesValue)
      .filter(getSecretKeyKeyword()::equals)
      .isPresent() && isStringValue(keyValuePair.value());
  }

  private Optional getIllegalKeywordArgument(CallExpression callExpression) {
    return Optional.ofNullable(TreeUtils.argumentByKeyword(getSecretKeyKeyword(), callExpression.arguments()))
      .filter(argument -> Optional.of(argument)
        .map(RegularArgument::expression)
        .filter(FlaskHardCodedSecret::isStringValue)
        .isPresent());
  }

  private void verifyAssignmentStatement(SubscriptionContext ctx) {
    AssignmentStatement assignmentStatementTree = (AssignmentStatement) ctx.syntaxNode();
    if (!isStringValue(assignmentStatementTree.assignedValue())) {
      return;
    }
    List expressionList = assignmentStatementTree.lhsExpressions().stream()
      .map(ExpressionList::expressions)
      .flatMap(List::stream)
      .filter(this::isSensitiveProperty)
      .toList();
    if (!expressionList.isEmpty()) {
      PreciseIssue issue = ctx.addIssue(assignmentStatementTree.assignedValue(), getMessage());
      expressionList.forEach(expr -> issue.secondary(expr, SECONDARY_MESSAGE));
    }
  }

  protected boolean isSensitiveProperty(Expression expression) {
    if (!expression.is(Tree.Kind.SUBSCRIPTION)) {
      return false;
    }
    return Optional.of((SubscriptionExpression) expression)
      .map(SubscriptionExpression::object)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
      .map(QualifiedExpression::symbol)
      .map(Symbol::fullyQualifiedName)
      .filter(FLASK_APP_CONFIG_QUALIFIER_FQNS::contains)
      .map(fqn -> ((SubscriptionExpression) expression).subscripts())
      .map(ExpressionList::expressions)
      .filter(list -> list.size() == 1)
      .map(list -> list.get(0))
      .map(FlaskHardCodedSecret::getAssignedValue)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(StringLiteral.class))
      .map(StringLiteral::trimmedQuotesValue)
      .filter(getSecretKeyKeyword()::equals)
      .isPresent();
  }

  private static boolean isStringValue(@Nullable Expression expr) {
    return isStringValue(expr, new HashSet<>());
  }


  private static boolean isStringValue(@Nullable Expression expr, Set visited) {
    if (expr == null) {
      return false;
    }
    if (expr.is(Tree.Kind.NAME)) {
      if (visited.contains(((Name) expr).name())) {
        return false;
      }
      visited.add(((Name) expr).name());
      Expression assignmentValueExpression = Expressions.singleAssignedValue((Name) expr);
      return isStringValue(assignmentValueExpression, visited);
    } else {
      return expr.is(Tree.Kind.STRING_LITERAL);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy