app/collectors/vpc.scala (176 lines of code) (raw):

package collectors import agent._ import conf.AWS import controllers.routes import play.api.mvc.Call import software.amazon.awssdk.services.ec2.Ec2Client import software.amazon.awssdk.services.ec2.model.{ DescribeSubnetsRequest, DescribeVpcsRequest, Filter, Subnet => AwsSubnet, Vpc => AwsVpc } import utils.Logging import scala.jdk.CollectionConverters._ import scala.language.postfixOps import scala.util.Try import software.amazon.awssdk.services.ec2.model.DescribeRouteTablesRequest class VpcCollectorSet(accounts: Accounts) extends CollectorSet[Vpc](ResourceType("Vpc"), accounts, Some(Regional)) { val lookupCollector: PartialFunction[Origin, Collector[Vpc]] = { case amazon: AmazonOrigin => AWSVpcCollector(amazon, resource, amazon.crawlRate(resource.name)) } } case class RouteTable( isMain: Boolean, subnetIDs: Set[String], hasInternetGateway: Boolean ) case class AWSVpcCollector( origin: AmazonOrigin, resource: ResourceType, crawlRate: CrawlRate ) extends Collector[Vpc] with Logging { val client: Ec2Client = Ec2Client.builder .credentialsProvider(origin.credentials.provider) .region(origin.awsRegionV2) .overrideConfiguration(AWS.clientConfig) .build def getSubnets(vpcId: String): Iterable[AwsSubnet] = { val subnetsRequest = DescribeSubnetsRequest.builder .filters(Filter.builder.name("vpc-id").values(vpcId).build) .build client.describeSubnetsPaginator(subnetsRequest).subnets.asScala } // Returns a map of subnetId to scope (public or private). def getSubnetScopes( vpcId: String, subnets: List[AwsSubnet] ): Map[String, SubnetScope] = { val req = DescribeRouteTablesRequest .builder() .filters(Filter.builder().name("vpc-id").values(vpcId).build) .build val tablesData = client.describeRouteTablesPaginator(req).routeTables().asScala // Let's convert the AWS data into something more useful for our purposes. val tables = tablesData.map(table => { val assocs = table.associations().asScala.toList val routes = table.routes().asScala.toList val isMain = assocs.exists(assoc => assoc.main()) // It feels like there should be a better way to detect the presence of an AWS Internet Gateway but apparently this is it :(. val tableHasIgw = routes.exists(route => Option(route.gatewayId()).getOrElse("").startsWith("igw") ) val subnetIDs = assocs.flatMap(assoc => Option(assoc.subnetId()).toList) RouteTable( isMain = isMain, hasInternetGateway = tableHasIgw, subnetIDs = subnetIDs.toSet ) }) val data = subnets.map(subnet => { // If there is no explicit route table associated with a subnet, the VPC 'main' route table is used instead. val main = tables.find(table => table.isMain) val associatedTable = tables.find(table => table.subnetIDs.contains(subnet.subnetId)) val isPublic = associatedTable.orElse(main).exists(table => table.hasInternetGateway) val scope = if (isPublic) Public else Private (subnet.subnetId -> scope) }) data.toMap } def crawl: Iterable[Vpc] = client .describeVpcsPaginator(DescribeVpcsRequest.builder.build) .vpcs .asScala .map { vpc => val subnets = getSubnets(vpc.vpcId).toList Vpc.fromApiData( vpc, subnets, getSubnetScopes(vpc.vpcId, subnets), origin ) } } sealed trait SubnetScope case object Public extends SubnetScope case object Private extends SubnetScope case object Unknown extends SubnetScope object Vpc { val UNUSABLE_IPS_IN_CIDR_BLOCK = 5 def countFromCidr(cidr: String): Option[Long] = { cidr.split("/").tail.headOption.flatMap { mask => val hostBits = 32 - mask.toInt Try { math.pow(2, hostBits).toLong - UNUSABLE_IPS_IN_CIDR_BLOCK }.toOption } } def arn(region: String, accountNumber: String, vpcId: String) = s"arn:aws:ec2:$region:$accountNumber:vpc/$vpcId" def fromApiData( vpc: AwsVpc, subnets: Iterable[AwsSubnet], subnetScopes: Map[String, SubnetScope], origin: AmazonOrigin ): Vpc = Vpc( arn = arn(origin.region, vpc.ownerId, vpc.vpcId), vpcId = vpc.vpcId, accountId = vpc.ownerId, state = vpc.stateAsString, cidrBlock = vpc.cidrBlock, default = vpc.isDefault, tenancy = vpc.instanceTenancyAsString, subnets = subnets.toList.map { s => Subnet( s.subnetArn, s.availabilityZone, s.cidrBlock, s.stateAsString, s.subnetId, s.ownerId, s.availableIpAddressCount, capacityIpAddressCount = countFromCidr(s.cidrBlock), s.tags.asScala.map(t => t.key -> t.value).toMap, isPublic = subnetScopes.getOrElse(s.subnetId(), Unknown) == Public ) }, availableIpAddressSum = subnets.map(_.availableIpAddressCount.toLong).sum, capacityIpAddressSum = { val counts = subnets.map(_.cidrBlock).flatMap(countFromCidr) if (counts.nonEmpty) Some(counts.sum) else None }, tags = vpc.tags.asScala.map(t => t.key -> t.value).toMap ) } case class Subnet( subnetArn: String, availabilityZone: String, cidrBlock: String, state: String, subnetId: String, ownerId: String, availableIpAddressCount: Int, capacityIpAddressCount: Option[Long], tags: Map[String, String] = Map.empty, isPublic: Boolean // Whether the Subnet is public or private, where public means it has an internet gateway in its route table. ) case class Vpc( arn: String, vpcId: String, accountId: String, state: String, cidrBlock: String, default: Boolean, tenancy: String, subnets: List[Subnet], availableIpAddressSum: Long, capacityIpAddressSum: Option[Long], tags: Map[String, String] = Map.empty ) extends IndexedItemWithStage with IndexedItemWithStack { def callFromArn: String => Call = arn => routes.Api.vpcs(arn) }