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

org.sonar.python.checks.cdk.UnrestrictedAdministrationCheckPartCfnSecurity 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.cdk;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import org.sonar.plugins.python.api.SubscriptionContext;
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.NumericLiteral;
import org.sonar.plugins.python.api.tree.StringLiteral;

import static org.sonar.python.checks.cdk.CdkPredicate.isNumericLiteral;
import static org.sonar.python.checks.cdk.CdkPredicate.isString;
import static org.sonar.python.checks.cdk.CdkPredicate.isStringLiteral;
import static org.sonar.python.checks.cdk.CdkUtils.getArgument;
import static org.sonar.python.checks.cdk.CdkUtils.getCall;
import static org.sonar.python.checks.cdk.CdkUtils.getDictionary;

public class UnrestrictedAdministrationCheckPartCfnSecurity extends AbstractCdkResourceCheck {
  private static final String MESSAGE = "Change this IP range to a subset of trusted IP addresses.";

  private static final String IP_PROTOCOL = "ip_protocol";
  private static final String CIDR_IP = "cidr_ip";
  private static final String CIDR_IPV6 = "cidr_ipv6";
  private static final String IPPROTOCOL = "ipProtocol";
  private static final String CIDRIP = "cidrIp";
  private static final String CIDRIPV6 = "cidrIpv6";


  private static final Set SENSITIVE_PROTOCOL = Set.of("tcp", "6");
  private static final String ANY_PROTOCOL = "-1";
  private static final String EMPTY_IPV4 = "0.0.0.0/0";
  private static final String EMPTY_IPV6 = "::/0";
  private static final long[] ADMIN_PORTS = new long[]{22, 3389};

  @Override
  protected void registerFqnConsumer() {
    checkFqn("aws_cdk.aws_ec2.CfnSecurityGroup", UnrestrictedAdministrationCheckPartCfnSecurity::checkCfnSecurityGroup);
    checkFqn("aws_cdk.aws_ec2.CfnSecurityGroupIngress", UnrestrictedAdministrationCheckPartCfnSecurity::checkCallCfnSecuritySensitive);
  }

  // Checks methods
  private static void checkCfnSecurityGroup(SubscriptionContext ctx, CallExpression callExpression) {
    getArgument(ctx, callExpression, "security_group_ingress")
      .flatMap(CdkUtils::getListExpression)
      .map(list -> list.elements().expressions())
      .orElse(Collections.emptyList())
      .stream()
      .map(expression -> CdkUtils.ExpressionFlow.build(ctx, expression))
      .forEach(flow -> {
        raiseIssueIfIngressPropertyCallWithSensitiveArgument(ctx, flow.getLast());
        raiseIssueIfDictionaryWithSensitiveArgument(ctx, flow.getLast());
      });
  }

  // Methods to handle call expressions
  private static void raiseIssueIfIngressPropertyCallWithSensitiveArgument(SubscriptionContext ctx, Expression expression) {
    getCall(expression, "aws_cdk.aws_ec2.CfnSecurityGroup.IngressProperty")
      .ifPresent(callExpression -> checkCallCfnSecuritySensitive(ctx, callExpression));
  }

  private static void checkCallCfnSecuritySensitive(SubscriptionContext ctx, CallExpression callExpression) {
    if (isCallWithArgumentBadProtocolEmptyIpAddressAdminPort(ctx, callExpression) || isCallWithArgumentInvalidProtocolEmptyIpAddress(ctx, callExpression)) {
      getArgument(ctx, callExpression, CIDR_IP).ifPresent(flow -> flow.addIssue(MESSAGE));
      getArgument(ctx, callExpression, CIDR_IPV6).ifPresent(flow -> flow.addIssue(MESSAGE));
    }
  }

  private static boolean isCallWithArgumentBadProtocolEmptyIpAddressAdminPort(SubscriptionContext ctx, CallExpression call) {
    return getArgument(ctx, call, IP_PROTOCOL).filter(flow -> flow.hasExpression(isString(SENSITIVE_PROTOCOL))).isPresent()
      && (getArgument(ctx, call, CIDR_IP).filter(flow -> flow.hasExpression(isString(EMPTY_IPV4))).isPresent()
        || getArgument(ctx, call, CIDR_IPV6).filter(flow -> flow.hasExpression(isString(EMPTY_IPV6))).isPresent())
      && hasSensitivePortRange(call, "from_port", "to_port", ADMIN_PORTS);
  }

  private static boolean isCallWithArgumentInvalidProtocolEmptyIpAddress(SubscriptionContext ctx, CallExpression call) {
    return getArgument(ctx, call, IP_PROTOCOL).filter(flow -> flow.hasExpression(isString(ANY_PROTOCOL))).isPresent()
      && (getArgument(ctx, call, CIDR_IP).filter(flow -> flow.hasExpression(isString(EMPTY_IPV4))).isPresent()
      || getArgument(ctx, call, CIDR_IPV6).filter(flow -> flow.hasExpression(isString(EMPTY_IPV6))).isPresent());
  }

  // Methods to handle dictionaries
  private static void raiseIssueIfDictionaryWithSensitiveArgument(SubscriptionContext ctx, Expression expression) {
    getDictionary(expression)
      .ifPresent(dictionary -> {
        DictionaryAsMap map = DictionaryAsMap.build(ctx, dictionary);

        if (isDictionaryWithAttributeBadProtocolEmptyIpAddressAdminPort(map) || isDictionaryWithAttributeInvalidProtocolEmptyIpAddress(map)) {
          map.addIssue(CIDRIP, MESSAGE);
          map.addIssue(CIDRIPV6, MESSAGE);
        }
      });
  }

  private static boolean isDictionaryWithAttributeBadProtocolEmptyIpAddressAdminPort(DictionaryAsMap map) {
    return map.hasKeyValuePair(IPPROTOCOL, isString(SENSITIVE_PROTOCOL))
      && (map.hasKeyValuePair(CIDRIP, isString(EMPTY_IPV4)) || map.hasKeyValuePair(CIDRIPV6, isString(EMPTY_IPV6)))
      && map.hasSensitivePortRange("fromPort", "toPort");
  }

  private static boolean isDictionaryWithAttributeInvalidProtocolEmptyIpAddress(DictionaryAsMap map) {
    return map.hasKeyValuePair(IPPROTOCOL, isString(ANY_PROTOCOL))
      && (map.hasKeyValuePair(CIDRIP, isString(EMPTY_IPV4)) || map.hasKeyValuePair(CIDRIPV6, isString(EMPTY_IPV6)));
  }

  // Class to handle Dictionary elements as a Map of ResolvedKeyValuePair, useful in case we need to refer to several elements and not only looking for a specific one.
  // The keys are all resolved String in CdkUtils.ResolvedKeyValuePair.key
  static class DictionaryAsMap {
    Map map = new HashMap<>();

    public static DictionaryAsMap build(SubscriptionContext ctx, DictionaryLiteral dictionary) {
      DictionaryAsMap dict = new DictionaryAsMap();

      List pairs = dictionary.elements().stream()
        .map(e -> CdkUtils.getKeyValuePair(ctx, e))
        .filter(Optional::isPresent).map(Optional::get).toList();

      for (CdkUtils.ResolvedKeyValuePair pair : pairs) {
        pair.key.getExpression(isStringLiteral()).map(StringLiteral.class::cast)
          .ifPresent(key -> dict.map.put(key.trimmedQuotesValue(), pair));
      }
      return dict;
    }

    public boolean hasKeyValuePair(String key, Predicate valuePredicate) {
      return map.containsKey(key) && map.get(key).value.hasExpression(valuePredicate);
    }

    public Optional get(String key, Predicate valuePredicate) {
      if (!map.containsKey(key)) {
        return Optional.empty();
      }
      return map.get(key).value.getExpression(valuePredicate);
    }

    public Optional getKeyString(String key) {
      return Optional.ofNullable(map.get(key))
        .flatMap(keyValuePair -> keyValuePair.key.getExpression(isStringLiteral()));
    }

    public Optional getValue(String key) {
      return Optional.ofNullable(map.get(key)).map(keyValuePair -> keyValuePair.value);
    }

    public void addIssue(String key, String message) {
      if (map.containsKey(key)) {
        map.get(key).value.addIssue(message);
      }
    }

    public Optional getArgumentAsLong(String name) {
      return get(name, isNumericLiteral())
        .map(NumericLiteral.class::cast)
        .map(NumericLiteral::valueAsLong);
    }

    boolean hasSensitivePortRange(String minName, String maxName) {
      Optional min = getArgumentAsLong(minName);
      Optional max = getArgumentAsLong(maxName);

      if (min.isEmpty() || max.isEmpty()) {
        return false;
      }

      return isInInterval(min.get(), max.get(), ADMIN_PORTS);
    }
  }

  // Utils method to work on arguments
  private static Optional getArgumentAsLong(CallExpression callExpression, String name) {
    return getArgument(null, callExpression, name)
      .flatMap(flow -> flow.getExpression(isNumericLiteral()))
      .map(NumericLiteral.class::cast)
      .map(NumericLiteral::valueAsLong);
  }

  private static boolean hasSensitivePortRange(CallExpression callExpression, String minName, String maxName, long[] numbers) {
    Optional min = getArgumentAsLong(callExpression, minName);
    Optional max = getArgumentAsLong(callExpression, maxName);

    if (min.isEmpty() || max.isEmpty()) {
      return false;
    }

    return isInInterval(min.get(), max.get(), numbers);
  }

  public static boolean isInInterval(long min, long max, long[] numbers) {
    for (long port : numbers) {
      if (min <= port && port <= max) {
        return true;
      }
    }
    return false;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy