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

org.sonar.python.checks.hotspots.AbstractCookieFlagCheck 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.hotspots;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.Kind;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.tree.TreeUtils;

import static org.sonar.python.checks.utils.Expressions.isFalsy;

public abstract class AbstractCookieFlagCheck extends PythonSubscriptionCheck {

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Kind.ASSIGNMENT_STMT, ctx -> {
      AssignmentStatement assignment = (AssignmentStatement) ctx.syntaxNode();
      getSubscriptionToCookies(assignment.lhsExpressions())
        .forEach(sub -> {
          if (isSettingFlag(sub, flagName()) && isFalsy(assignment.assignedValue())) {
            ctx.addIssue(assignment, message());
          }
        });
    });

    context.registerSyntaxNodeConsumer(Kind.CALL_EXPR, ctx -> {
      CallExpression callExpression = (CallExpression) ctx.syntaxNode();
      verifyCallExpression(ctx, callExpression);
    });
  }

  private void verifyCallExpression(SubscriptionContext ctx, CallExpression callExpression) {
    if (callExpression.arguments().stream().anyMatch(argument -> argument.is(Kind.UNPACKING_EXPR))) {
      return;
    }
    var registry = methodArgumentsToCheckRegistry();

    var methodName = getCallExpressionMethodName(callExpression);
    var methodFqn = getCallExpressionMethodFqn(callExpression);
    if ((methodName != null && registry.hasMethodName(methodName)) || (methodFqn != null && registry.hasMethodFqn(methodFqn))) {
      findMethodArgumentToCheck(callExpression, methodFqn, methodName)
        .ifPresent(methodArgumentsToCheck -> {
          var argument = TreeUtils.nthArgumentOrKeyword(methodArgumentsToCheck.argumentPosition(), methodArgumentsToCheck.argumentName(), callExpression.arguments());
          if (methodArgumentsToCheck.invalidArgumentPredicate().test(argument)) {
            ctx.addIssue(callExpression.callee(), message());
          }
        });
    }
  }

  private Optional findMethodArgumentToCheck(CallExpression callExpression, @Nullable String methodFqn, @Nullable String methodName) {
    return Optional.ofNullable(methodFqn)
      .map(methodArgumentsToCheckRegistry()::getByMethodFqn)
      .or(() -> Optional.ofNullable(methodName)
          .map(methodArgumentsToCheckRegistry()::getByMethodName)
          .stream()
          .flatMap(Collection::stream)
          .filter(methodArgumentsToCheck -> canBeOrExtendMatches(callExpression, methodArgumentsToCheck))
          .findFirst()
      );
  }

  private static Boolean canBeOrExtendMatches(CallExpression callExpression, MethodArgumentsToCheck methodArgumentsToCheck) {
    return Optional.of(callExpression)
      .map(CallExpression::callee)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
      .map(QualifiedExpression::qualifier)
      .map(Expression::type)
      .map(t -> t.mustBeOrExtend(methodArgumentsToCheck.calleeFqn()))
      .orElse(false);
  }

  private static String getCallExpressionMethodName(CallExpression callExpression) {
    return Optional.of(callExpression)
      .map(CallExpression::calleeSymbol)
      .map(Symbol::name)
      .orElse(null);
  }

  private static String getCallExpressionMethodFqn(CallExpression callExpression) {
    return Optional.of(callExpression)
      .map(CallExpression::calleeSymbol)
      .map(Symbol::fullyQualifiedName)
      .orElse(null);
  }

  private static Stream getSubscriptionToCookies(List lhsExpressions) {
    return lhsExpressions.stream()
      .flatMap(expressionList -> expressionList.expressions().stream())
      .filter(lhs -> {
        if (lhs.is(Kind.SUBSCRIPTION)) {
          SubscriptionExpression sub = (SubscriptionExpression) lhs;
          return getObject(sub.object()).type().canOnlyBe("http.cookies.SimpleCookie");
        }
        return false;
      })
      .map(SubscriptionExpression.class::cast);
  }

  private static boolean isSettingFlag(SubscriptionExpression sub, String flagName) {
    List subscripts = getSubscripts(sub);
    if (subscripts.size() == 1) {
      return false;
    }
    return subscripts.stream()
      .skip(1)
      .anyMatch(s -> s.expressions().size() == 1 && isFlagNameStringLiteral(s.expressions().get(0), flagName));
  }

  private static List getSubscripts(SubscriptionExpression sub) {
    Deque subscripts = new ArrayDeque<>();
    subscripts.addFirst(sub.subscripts());
    Expression object = sub.object();
    while (object.is(Kind.SUBSCRIPTION)) {
      subscripts.addFirst(((SubscriptionExpression) object).subscripts());
      object = ((SubscriptionExpression) object).object();
    }
    return new ArrayList<>(subscripts);
  }

  private static boolean isFlagNameStringLiteral(Expression expression, String value) {
    return expression.is(Kind.STRING_LITERAL) && ((StringLiteral) expression).trimmedQuotesValue().equalsIgnoreCase(value);
  }

  private static Expression getObject(Expression object) {
    if (object.is(Kind.SUBSCRIPTION)) {
      return getObject(((SubscriptionExpression) object).object());
    }
    return object;
  }

  protected boolean isInvalidHeaderArgument(@Nullable RegularArgument argument) {
    return Optional.ofNullable(argument)
      .map(RegularArgument::expression)
      .map(this::isDictWithSensitiveEntry)
      .orElse(false);
  }

  private boolean isDictWithSensitiveEntry(Expression expression) {
    return TreeUtils.toOptionalInstanceOf(Name.class, expression)
      .flatMap(Expressions::singleAssignedNonNameValue)
      .map(this::isDictWithSensitiveEntry)
      .or(() -> TreeUtils.toOptionalInstanceOf(DictionaryLiteral.class, expression)
        .map(this::hasInvalidEntry)
      ).orElse(false);
  }

  private boolean hasInvalidEntry(DictionaryLiteral dictionaryLiteral) {
    return dictionaryLiteral.elements().stream()
      .filter(KeyValuePair.class::isInstance)
      .map(KeyValuePair.class::cast)
      .filter(keyValuePair -> isSensitiveKey(keyValuePair.key()))
      .map(KeyValuePair::value)
      .anyMatch(this::invalidValue);
  }

  private static boolean isSensitiveKey(Expression key) {
    return TreeUtils.toOptionalInstanceOf(StringLiteral.class, key)
      .map(StringLiteral::trimmedQuotesValue)
      .filter("set-cookie"::equalsIgnoreCase)
      .isPresent();
  }

  private boolean invalidValue(Expression value) {
    return TreeUtils.toOptionalInstanceOf(StringLiteral.class, value)
      .map(StringLiteral::trimmedQuotesValue)
      .filter(Predicate.not(val -> val.matches(headerValueRegex())))
      .isPresent();
  }

  protected abstract String headerValueRegex();

  abstract String flagName();

  abstract String message();

  abstract MethodArgumentsToCheckRegistry methodArgumentsToCheckRegistry();

  static class MethodArgumentsToCheckRegistry {
    private final Map> byMethodName;
    private final Map byMethodFqn;

    public MethodArgumentsToCheckRegistry(MethodArgumentsToCheck... arguments) {
      byMethodName = Stream.of(arguments)
        .filter(argument -> Objects.nonNull(argument.methodName))
        .collect(Collectors.groupingBy(MethodArgumentsToCheck::methodName));

      byMethodFqn = Stream.of(arguments)
        .collect(Collectors.toMap(MethodArgumentsToCheck::methodFqn, Function.identity(), (v1, v2) -> v2));
    }

    List getByMethodName(String methodName) {
      return byMethodName.get(methodName);
    }

    MethodArgumentsToCheck getByMethodFqn(String methodFqn) {
      return byMethodFqn.get(methodFqn);
    }

    boolean hasMethodName(String methodName) {
      return byMethodName.containsKey(methodName);
    }

    boolean hasMethodFqn(String methodFqn) {
      return byMethodFqn.containsKey(methodFqn);
    }
  }

  static class MethodArgumentsToCheck {
    private final String calleeFqn;
    private final String methodName;
    private final String methodFqn;
    private final String argumentName;
    private final int argumentPosition;
    private final Predicate invalidArgumentPredicate;


    public MethodArgumentsToCheck(String calleeFqn, String argumentName, int argumentPosition, Predicate invalidArgumentPredicate) {
      this(calleeFqn, null, argumentName, argumentPosition, invalidArgumentPredicate);
    }

    public MethodArgumentsToCheck(String calleeFqn, String methodName, String argumentName, int argumentPosition) {
      this(calleeFqn, methodName, argumentName, argumentPosition, (@Nullable RegularArgument arg) -> (arg == null || isFalsy(arg.expression())));
    }

    public MethodArgumentsToCheck(String calleeFqn, @Nullable String methodName, String argumentName, int argumentPosition, Predicate invalidArgumentPredicate) {
      this.calleeFqn = calleeFqn;
      this.methodName = methodName;
      this.invalidArgumentPredicate = invalidArgumentPredicate;
      methodFqn = Optional.ofNullable(methodName)
        .map(mn -> calleeFqn + "." + mn)
        .orElse(calleeFqn);
      this.argumentName = argumentName;
      this.argumentPosition = argumentPosition;
    }

    public String calleeFqn() {
      return calleeFqn;
    }

    public String methodName() {
      return methodName;
    }

    public String argumentName() {
      return argumentName;
    }

    public int argumentPosition() {
      return argumentPosition;
    }

    public String methodFqn() {
      return methodFqn;
    }

    public Predicate invalidArgumentPredicate() {
      return invalidArgumentPredicate;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy