providers/aws/sg.go (343 lines of code) (raw):

// Copyright 2018 The Terraformer Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package aws import ( "bytes" "context" "fmt" "os" "sort" "strings" "github.com/GoogleCloudPlatform/terraformer/terraformutils" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/hashicorp/terraform/flatmap" "gonum.org/v1/gonum/graph" simplegraph "gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/topo" ) var SgAllowEmptyValues = []string{"tags."} type void struct{} var member void type SecurityGenerator struct { AWSService } type ByGroupPair []types.UserIdGroupPair func (b ByGroupPair) Len() int { return len(b) } func (b ByGroupPair) Swap(i, j int) { b[i], b[j] = b[j], b[i] } func (b ByGroupPair) Less(i, j int) bool { if b[i].GroupId != nil && b[j].GroupId != nil { return *b[i].GroupId < *b[j].GroupId } if b[i].GroupName != nil && b[j].GroupName != nil { return *b[i].GroupName < *b[j].GroupName } panic("mismatched security group rules, may be a terraform bug") } func (SecurityGenerator) createResources(securityGroups []types.SecurityGroup) []terraformutils.Resource { var sgIDsToMoveOut []string _, shouldSplitRules := os.LookupEnv("SPLIT_SG_RULES") if shouldSplitRules { for _, sg := range securityGroups { sgIDsToMoveOut = append(sgIDsToMoveOut, *sg.GroupId) } } else { sgIDsToMoveOut = findSgsToMoveOut(securityGroups) } var resources []terraformutils.Resource for _, sg := range securityGroups { if sg.VpcId == nil { continue } ruleAttributes := map[string]interface{}{} // we must move out all of the rules - https://github.com/hashicorp/terraform/issues/11011#issuecomment-283076580 for _, groupIDToMoveOut := range sgIDsToMoveOut { if groupIDToMoveOut == *sg.GroupId { ruleAttributes["clearRules"] = true for _, rule := range sg.IpPermissions { resources = processRule(rule, "ingress", sg, resources) } for _, rule := range sg.IpPermissionsEgress { resources = processRule(rule, "egress", sg, resources) } } } resources = append(resources, terraformutils.NewResource( StringValue(sg.GroupId), strings.Trim(StringValue(sg.GroupName)+"_"+StringValue(sg.GroupId), " "), "aws_security_group", "aws", map[string]string{}, SgAllowEmptyValues, ruleAttributes)) } return resources } func processRule(rule types.IpPermission, ruleType string, sg types.SecurityGroup, resources []terraformutils.Resource) []terraformutils.Resource { if rule.UserIdGroupPairs != nil && len(rule.UserIdGroupPairs) > 0 { if len(rule.IpRanges) > 0 { // we must unwind coupled CIDR IPv4 range + security group rules attributes := baseRuleAttributes(ruleType, rule, sg) resources = append(resources, terraformutils.NewResource( permissionID(*sg.GroupId, ruleType, "", rule), permissionID(*sg.GroupId, ruleType, "", rule), "aws_security_group_rule", "aws", flatmap.Flatten(attributes), SgAllowEmptyValues, map[string]interface{}{})) } if len(rule.Ipv6Ranges) > 0 { // we must unwind coupled CIDR IPv6 range + security group rules attributes := baseRuleAttributes(ruleType, rule, sg) resources = append(resources, terraformutils.NewResource( permissionID(*sg.GroupId, ruleType, "", rule), permissionID(*sg.GroupId, ruleType, "", rule), "aws_security_group_rule", "aws", flatmap.Flatten(attributes), SgAllowEmptyValues, map[string]interface{}{})) } for _, groupPair := range rule.UserIdGroupPairs { attributes := baseRuleAttributes(ruleType, rule, sg) delete(attributes, "cidr_blocks") delete(attributes, "ipv6_cidr_blocks") if *groupPair.GroupId == *sg.GroupId { // Solution to C1 attributes["self"] = true } else { attributes["source_security_group_id"] = *groupPair.GroupId } resources = append(resources, terraformutils.NewResource( permissionID(*sg.GroupId, ruleType, *groupPair.GroupId, rule), permissionID(*sg.GroupId, ruleType, *groupPair.GroupId, rule), "aws_security_group_rule", "aws", flatmap.Flatten(attributes), SgAllowEmptyValues, map[string]interface{}{})) } } else { attributes := baseRuleAttributes(ruleType, rule, sg) resources = append(resources, terraformutils.NewResource( permissionID(*sg.GroupId, ruleType, "", rule), permissionID(*sg.GroupId, ruleType, "", rule), "aws_security_group_rule", "aws", flatmap.Flatten(attributes), SgAllowEmptyValues, map[string]interface{}{})) } return resources } func baseRuleAttributes(ruleType string, rule types.IpPermission, sg types.SecurityGroup) map[string]interface{} { attributes := map[string]interface{}{ "type": ruleType, "cidr_blocks": ipRange(rule), "ipv6_cidr_blocks": ip6Range(rule), "prefix_list_ids": prefixes(rule), "from_port": fromPort(rule), "protocol": *rule.IpProtocol, "security_group_id": *sg.GroupId, "to_port": toPort(rule), } return attributes } // Let's try to find all cycles by applying Johnson's method on the directed graph // We cannot build a line graph and move out only rules because of hashicorp/terraform#11011 func findSgsToMoveOut(securityGroups []types.SecurityGroup) []string { // Vertexes are security groups, edges are rules. The task is to find correct set of rule definitions, so that we // won't have cycles sourceGraph := simplegraph.NewDirectedGraph() idToSg := make(map[int]types.SecurityGroup) sgToIdx := make(map[string]int64) for idx, sg := range securityGroups { idToSg[idx] = sg sgToIdx[StringValue(sg.GroupId)] = int64(idx) sourceGraph.AddNode(sourceGraph.NewNode()) } for idx, sg := range securityGroups { for _, rule := range sg.IpPermissions { pairs := rule.UserIdGroupPairs for _, pair := range pairs { if pair.GroupId != nil { fromNode := sourceGraph.Node(int64(idx)) toNode := sourceGraph.Node(sgToIdx[StringValue(pair.GroupId)]) if fromNode.ID() != toNode.ID() { sourceGraph.SetEdge(sourceGraph.NewEdge(fromNode, toNode)) } } } } } cyclesInLineGraph := topo.DirectedCyclesIn(sourceGraph) // C1 cycles won't be found but Terraform solves that issue resultingSet := make(map[string]void) for _, v := range cyclesInLineGraph { if elementAlreadyFound(resultingSet, v, idToSg) { continue } // Try to move out node with lowest number of rules group := idToSg[int(v[0].ID())] for _, vi := range v { viGroup := idToSg[int(vi.ID())] if len(viGroup.IpPermissions) < len(group.IpPermissions) { group = viGroup } } resultingSet[*group.GroupId] = member } result := make([]string, len(resultingSet)) i := 0 for k := range resultingSet { result[i] = k i++ } return result } func elementAlreadyFound(resultingSet map[string]void, v []graph.Node, idToSg map[int]types.SecurityGroup) bool { for k := range resultingSet { for _, vi := range v { viGroupID := *idToSg[int(vi.ID())].GroupId if k == viGroupID { return true } } } return false } func (g *SecurityGenerator) InitResources() error { config, err := g.generateConfig() if err != nil { return err } svc := ec2.NewFromConfig(config) p := ec2.NewDescribeSecurityGroupsPaginator(svc, &ec2.DescribeSecurityGroupsInput{}) var resourcesToFilter []types.SecurityGroup for p.HasMorePages() { page, err := p.NextPage(context.TODO()) if err != nil { return err } resourcesToFilter = append(resourcesToFilter, page.SecurityGroups...) } sort.Slice(resourcesToFilter, func(i, j int) bool { return *resourcesToFilter[i].GroupId < *resourcesToFilter[j].GroupId }) g.Resources = g.createResources(resourcesToFilter) return nil } func (g *SecurityGenerator) PostConvertHook() error { for _, resource := range g.Resources { if resource.InstanceInfo.Type == "aws_security_group_rule" { if resource.Item["self"] == "true" { delete(resource.Item, "source_security_group_id") } } else if resource.InstanceInfo.Type == "aws_security_group" { if resource.Item["clearRules"] == true { delete(resource.Item, "ingress") delete(resource.Item, "egress") delete(resource.Item, "clearRules") continue } if val, ok := resource.Item["ingress"]; ok { g.sortRules(val.([]interface{})) } if val, ok := resource.Item["egress"]; ok { g.sortRules(val.([]interface{})) } } } return nil } func (g *SecurityGenerator) sortRules(rules []interface{}) { for _, rule := range rules { ruleMap := rule.(map[string]interface{}) g.sortIfExist("cidr_blocks", ruleMap) g.sortIfExist("ipv6_cidr_blocks", ruleMap) g.sortIfExist("security_groups", ruleMap) } sort.Slice(rules, func(i, j int) bool { return fmt.Sprintf("%v", rules[i]) < fmt.Sprintf("%v", rules[j]) }) } func (g *SecurityGenerator) sortIfExist(attribute string, ruleMap map[string]interface{}) { if val, ok := ruleMap[attribute]; ok { sort.Slice(val.([]interface{}), func(i, j int) bool { return val.([]interface{})[i].(string) < val.([]interface{})[j].(string) }) } } func permissionID(sgID, ruleType, groupID string, ip types.IpPermission) string { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("%s_%s_%s_%d_%d_", sgID, ruleType, *ip.IpProtocol, fromPort(ip), toPort(ip))) if len(ip.IpRanges) > 0 { s := make([]string, len(ip.IpRanges)) for i, r := range ip.IpRanges { s[i] = *r.CidrIp } sort.Strings(s) for _, v := range s { buf.WriteString(fmt.Sprintf("%s_", v)) } } if len(ip.Ipv6Ranges) > 0 { s := make([]string, len(ip.Ipv6Ranges)) for i, r := range ip.Ipv6Ranges { s[i] = *r.CidrIpv6 } sort.Strings(s) for _, v := range s { buf.WriteString(fmt.Sprintf("%s_", v)) } } if len(ip.PrefixListIds) > 0 { s := make([]string, len(ip.PrefixListIds)) for i, pl := range ip.PrefixListIds { s[i] = *pl.PrefixListId } sort.Strings(s) for _, v := range s { buf.WriteString(fmt.Sprintf("%s_", v)) } } if groupID != "" { buf.WriteString(fmt.Sprintf("%s_", groupID)) } idPreformatted := buf.String() return idPreformatted[:len(idPreformatted)-1] } func fromPort(ip types.IpPermission) int { switch { case *ip.IpProtocol == "icmp": return -1 case *ip.IpProtocol == "-1": return -1 case ip.FromPort != nil && *ip.FromPort > 0: return int(*ip.FromPort) default: return 0 } } func toPort(ip types.IpPermission) int { switch { case *ip.IpProtocol == "icmp": return -1 case *ip.IpProtocol == "-1": return -1 case ip.ToPort != nil && *ip.ToPort > 0: return int(*ip.ToPort) default: return 65536 } } func ipRange(rule types.IpPermission) []string { result := make([]string, len(rule.IpRanges)) for idx, rule := range rule.IpRanges { result[idx] = *rule.CidrIp } return result } func ip6Range(rule types.IpPermission) []string { result := make([]string, len(rule.Ipv6Ranges)) for idx, rule := range rule.Ipv6Ranges { result[idx] = *rule.CidrIpv6 } return result } func prefixes(rule types.IpPermission) []string { result := make([]string, len(rule.PrefixListIds)) for idx, rule := range rule.PrefixListIds { result[idx] = *rule.PrefixListId } return result }