diff --git a/iam/get.go b/iam/get.go index 875abd4..27fc80a 100644 --- a/iam/get.go +++ b/iam/get.go @@ -73,9 +73,9 @@ func ListUsers(showOnly string) error { } for _, user := range data { if user.HasConsoleAccess() { - summaryStats["consoleAccess"] += 1 + summaryStats["consoleAccess"]++ } - summaryStats[user.CheckStatus()] += 1 + summaryStats[user.CheckStatus()]++ if showOnly == "" || showOnly == user.CheckStatus() { t.AppendRow([]interface{}{ @@ -101,9 +101,9 @@ func ListUsers(showOnly string) error { for _, key := range user.accessKeys { switch aws.StringValue(key.status) { case "Active": - summaryStats["activeKeys"] += 1 + summaryStats["activeKeys"]++ case "Inactive": - summaryStats["inactiveKeys"] += 1 + summaryStats["inactiveKeys"]++ } st.AppendRow([]interface{}{ aws.StringValue(key.id), diff --git a/main.go b/main.go index 70826e0..f137f84 100644 --- a/main.go +++ b/main.go @@ -138,6 +138,34 @@ throttling from AWS with an exponential backoff with retry. return nil }, }, + { + Name: "public", + Usage: "Produce report of instances that have public interfaces attached", + UsageText: ` +Produces a report that displays a list RDS servers that are configured as Publicly Accessible. + +The report contains: + +DB INSTANCE: + - Name of the instance + +ENGINE: + - RDS DB engine + +SECURITY GROUPS: + - Security Group ID + - Security Group Name + - Inbound Port + - CIDR rules applied to the Port +`, + Action: func(c *cli.Context) error { + err := rds.ListPublicInterfaces() + if err != nil { + return cli.NewExitError(err, 2) + } + return nil + }, + }, }, }, { diff --git a/rds/public.go b/rds/public.go new file mode 100644 index 0000000..9881cce --- /dev/null +++ b/rds/public.go @@ -0,0 +1,89 @@ +package rds + +import ( + "fmt" + "github.com/GoodwayGroup/gw-aws-audit/sg" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + as "github.com/clok/awssession" + "github.com/clok/kemba" + "github.com/jedib0t/go-pretty/v6/table" + "net" + "os" +) + +// ListPublicInterfaces will list RDS instances with a public interface attached. +func ListPublicInterfaces() error { + k := kemba.New("gw-aws-audit:rds:ListPublicInterfaces") + sess, err := as.New() + if err != nil { + return err + } + client := rds.New(sess) + cnt := 0 + + var result *rds.DescribeDBInstancesOutput + result, err = client.DescribeDBInstances(&rds.DescribeDBInstancesInput{}) + + if err != nil { + fmt.Println("Failed to list instances") + return err + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + t.AppendHeader(table.Row{"DB Instance", "Engine", "Security Groups"}) + + k.Printf("checking %d RDS instances", len(result.DBInstances)) + for _, db := range result.DBInstances { + if aws.BoolValue(db.PubliclyAccessible) { + cnt++ + + var sgIDs []*string + for _, sec := range db.VpcSecurityGroups { + sgIDs = append(sgIDs, sec.VpcSecurityGroupId) + } + sgs, err := sg.GetSecurityGroups(sgIDs) + if err != nil { + return err + } + var securityGroups []*sg.SecurityGroup + for _, sec := range sgs { + securityGroups = append(securityGroups, sec) + } + k.Log(securityGroups) + + var ips []string + var stub string + for _, sec := range securityGroups { + for token, rule := range sec.Rules() { + port, _, _ := sec.ParseRuleToken(token) + for _, ip := range rule { + _, ipv4Net, _ := net.ParseCIDR(aws.StringValue(ip.CidrIp)) + ips = append(ips, ipv4Net.String()) + } + stub = fmt.Sprintf("%s\t%s\t%s\n\n\t", sec.ID(), sec.Name(), port) + for i, ip := range ips { + if i != 0 && i%4 == 0 { + stub = fmt.Sprintf("%s\n\t", stub) + } + stub = fmt.Sprintf("%s %20s", stub, ip) + } + stub = fmt.Sprintf("%s\n", stub) + } + } + + name := aws.StringValue(db.DBInstanceIdentifier) + engine := aws.StringValue(db.Engine) + + t.AppendRow([]interface{}{name, engine, stub}) + } + } + + // There are a LOT of metrics to consider + // See: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html + t.AppendFooter(table.Row{"DB Instances", cnt}) + t.Render() + return nil +} diff --git a/sg/attached.go b/sg/attached.go index a4a14f1..201032f 100644 --- a/sg/attached.go +++ b/sg/attached.go @@ -18,7 +18,7 @@ func ListAttachedSecurityGroups() error { return err } - var attached []*securityGroup + var attached []*SecurityGroup for _, sg := range sgs { if sg.attached != nil { attached = append(attached, sg) diff --git a/sg/aws.go b/sg/aws.go index 0b29b72..81b4c2b 100644 --- a/sg/aws.go +++ b/sg/aws.go @@ -23,7 +23,7 @@ func GenerateExternalAWSIPReport() error { return err } - var securityGroups []*securityGroup + var securityGroups []*SecurityGroup for _, sg := range sgs { securityGroups = append(securityGroups, sg) } diff --git a/sg/detached.go b/sg/detached.go index 68504d8..c90f9ea 100644 --- a/sg/detached.go +++ b/sg/detached.go @@ -16,7 +16,7 @@ func ListDetachedSecurityGroups() error { return err } - var detached []*securityGroup + var detached []*SecurityGroup for _, sg := range sgs { if sg.attached == nil { detached = append(detached, sg) diff --git a/sg/get.go b/sg/get.go index af0d062..13eccff 100644 --- a/sg/get.go +++ b/sg/get.go @@ -13,17 +13,19 @@ var ( ksg = kemba.New("gw-aws-audit:sg") ) -func getAllSecurityGroups() (map[string]*securityGroup, error) { - kl := ksg.Extend("get-all-sg") +// GetSecurityGroups will retrieve a list of Security Group IDs with mapped ports +func GetSecurityGroups(sgIDs []*string) (map[string]*SecurityGroup, error) { + kl := ksg.Extend("get-sg") sess, err := as.New() if err != nil { return nil, err } client := ec2.New(sess) + kl.Printf("retrieving SG IDs: %# v", sgIDs) var results *ec2.DescribeSecurityGroupsOutput results, err = client.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ - MaxResults: aws.Int64(1000), + GroupIds: sgIDs, }) if err != nil { fmt.Println("Failed to list Security Groups") @@ -31,7 +33,13 @@ func getAllSecurityGroups() (map[string]*securityGroup, error) { } kl.Printf("found %d security groups", len(results.SecurityGroups)) - secGroups := make(map[string]*securityGroup, len(results.SecurityGroups)) + secGroups := processSecurityGroupsResponse(results) + + return secGroups, nil +} + +func processSecurityGroupsResponse(results *ec2.DescribeSecurityGroupsOutput) map[string]*SecurityGroup { + secGroups := make(map[string]*SecurityGroup, len(results.SecurityGroups)) for _, sec := range results.SecurityGroups { rules := map[string][]*ec2.IpRange{} for _, rule := range sec.IpPermissions { @@ -63,12 +71,34 @@ func getAllSecurityGroups() (map[string]*securityGroup, error) { } } - secGroups[aws.StringValue(sec.GroupId)] = &securityGroup{ + secGroups[aws.StringValue(sec.GroupId)] = &SecurityGroup{ id: aws.StringValue(sec.GroupId), name: aws.StringValue(sec.GroupName), rules: rules, } } + return secGroups +} + +func getAllSecurityGroups() (map[string]*SecurityGroup, error) { + kl := ksg.Extend("get-all-sg") + sess, err := as.New() + if err != nil { + return nil, err + } + client := ec2.New(sess) + + var results *ec2.DescribeSecurityGroupsOutput + results, err = client.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ + MaxResults: aws.Int64(1000), + }) + if err != nil { + fmt.Println("Failed to list Security Groups") + return nil, err + } + + kl.Printf("found %d security groups", len(results.SecurityGroups)) + secGroups := processSecurityGroupsResponse(results) return secGroups, nil } @@ -101,7 +131,7 @@ func buildPortToken(fromPort string, toPort string, proto *string, securityGroup return strings.Join(parts, "::") } -func detectAttachedSecurityGroups(sgs map[string]*securityGroup) error { +func detectAttachedSecurityGroups(sgs map[string]*SecurityGroup) error { kl := ksg.Extend("detect-attached") sess, err := as.New() if err != nil { @@ -171,7 +201,7 @@ func detectAttachedSecurityGroups(sgs map[string]*securityGroup) error { return nil } -func getAnnotatedSecurityGroups() (map[string]*securityGroup, error) { +func getAnnotatedSecurityGroups() (map[string]*SecurityGroup, error) { // get all sgs in a region sgs, err := getAllSecurityGroups() if err != nil { diff --git a/sg/helper.go b/sg/helper.go index 641fb7b..3400568 100644 --- a/sg/helper.go +++ b/sg/helper.go @@ -36,7 +36,7 @@ func generateReport(c *cli.Context, checkFxn func(a []string, b string) bool, po return err } - var securityGroups []*securityGroup + var securityGroups []*SecurityGroup for _, sg := range sgs { if sg.attached != nil || c.Bool("all") { securityGroups = append(securityGroups, sg) @@ -95,7 +95,7 @@ func parseToken(token string) (port string, protocol string, sgIDs string) { return parts[0], parts[1], parts[2] } -func processSecurityGroups(securityGroups []*securityGroup, groupedCIDRs *groupedIPBlockRules, checkFxn func(a []string, b string) bool, ports []string, ignoredProtocols map[string]bool) (*groupedSecurityGroups, error) { +func processSecurityGroups(securityGroups []*SecurityGroup, groupedCIDRs *groupedIPBlockRules, checkFxn func(a []string, b string) bool, ports []string, ignoredProtocols map[string]bool) (*groupedSecurityGroups, error) { kl := ksg.Extend("processSecurityGroups") mappedSGs := newGroupedSecurityGroups() @@ -208,7 +208,7 @@ func generateIPBlockRules(c *cli.Context) (*groupedIPBlockRules, error) { return groupedCIDRs, nil } -func printTable(data map[*securityGroup][]*portToIP) { +func printTable(data map[*SecurityGroup][]*portToIP) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) @@ -222,7 +222,7 @@ func printTable(data map[*securityGroup][]*portToIP) { if i == 0 { id = sec.id name = sec.name - usage = sec.getAttachmentsAsString() + usage = sec.GetAttachmentsAsString() } t.AppendRow([]interface{}{ id, @@ -238,7 +238,7 @@ func printTable(data map[*securityGroup][]*portToIP) { t.Render() } -func printAmazonTable(data map[*securityGroup][]*portToIP) { +func printAmazonTable(data map[*SecurityGroup][]*portToIP) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) @@ -252,7 +252,7 @@ func printAmazonTable(data map[*securityGroup][]*portToIP) { if i == 0 { id = sec.id name = sec.name - usage = sec.getAttachmentsAsString() + usage = sec.GetAttachmentsAsString() } t.AppendRow([]interface{}{ id, diff --git a/sg/types.go b/sg/types.go index dab028b..f4e133a 100644 --- a/sg/types.go +++ b/sg/types.go @@ -14,42 +14,71 @@ type portToIP struct { prefix *Prefix } -type securityGroup struct { +// SecurityGroup defines the struct for common SG properties used by this tool. +type SecurityGroup struct { id string name string attached map[string]int rules map[string][]*ec2.IpRange } -func (s *securityGroup) getAttachmentsAsString() string { +// GetAttachmentsAsString will return a formatted list of AWS attachments +func (s *SecurityGroup) GetAttachmentsAsString() string { var attachments []string - for t, cnt := range s.attached { + for t, cnt := range s.Attachments() { attachments = append(attachments, fmt.Sprintf("%s: %d", t, cnt)) } return strings.Join(attachments, " ") } +// Attachments will return the map of Attachments +func (s *SecurityGroup) Attachments() map[string]int { + return s.attached +} + +// ID will return the SecurityGroup ID +func (s *SecurityGroup) ID() string { + return s.id +} + +// Name will return the SecurityGroup Name +func (s *SecurityGroup) Name() string { + return s.name +} + +// Rules will return the SecurityGroup Rules map +func (s *SecurityGroup) Rules() map[string][]*ec2.IpRange { + return s.rules +} + +// ParseRuleToken break the Rules token key from the Rules map and return +// the component parts of [port, protocol, security group IDs] +func (s SecurityGroup) ParseRuleToken(token string) (port string, protocol string, sgIDs string) { + parts := strings.Split(token, "::") + return parts[0], parts[1], parts[2] +} + type groupedSecurityGroups struct { - alert map[*securityGroup][]*portToIP - warning map[*securityGroup][]*portToIP - unknown map[*securityGroup][]*portToIP - wideOpen map[*securityGroup][]*portToIP - amazon map[*securityGroup][]*portToIP - sg map[*securityGroup][]*portToIP + alert map[*SecurityGroup][]*portToIP + warning map[*SecurityGroup][]*portToIP + unknown map[*SecurityGroup][]*portToIP + wideOpen map[*SecurityGroup][]*portToIP + amazon map[*SecurityGroup][]*portToIP + sg map[*SecurityGroup][]*portToIP } func newGroupedSecurityGroups() *groupedSecurityGroups { var gsg groupedSecurityGroups - gsg.alert = map[*securityGroup][]*portToIP{} - gsg.warning = map[*securityGroup][]*portToIP{} - gsg.unknown = map[*securityGroup][]*portToIP{} - gsg.wideOpen = map[*securityGroup][]*portToIP{} - gsg.amazon = map[*securityGroup][]*portToIP{} - gsg.sg = map[*securityGroup][]*portToIP{} + gsg.alert = map[*SecurityGroup][]*portToIP{} + gsg.warning = map[*SecurityGroup][]*portToIP{} + gsg.unknown = map[*SecurityGroup][]*portToIP{} + gsg.wideOpen = map[*SecurityGroup][]*portToIP{} + gsg.amazon = map[*SecurityGroup][]*portToIP{} + gsg.sg = map[*SecurityGroup][]*portToIP{} return &gsg } -func (csg *groupedSecurityGroups) addToAlert(sec *securityGroup, value *portToIP) { +func (csg *groupedSecurityGroups) addToAlert(sec *SecurityGroup, value *portToIP) { if _, ok := csg.alert[sec]; !ok { csg.alert[sec] = []*portToIP{value} } else { @@ -57,7 +86,7 @@ func (csg *groupedSecurityGroups) addToAlert(sec *securityGroup, value *portToIP } } -func (csg *groupedSecurityGroups) addToWarning(sec *securityGroup, value *portToIP) { +func (csg *groupedSecurityGroups) addToWarning(sec *SecurityGroup, value *portToIP) { if _, ok := csg.warning[sec]; !ok { csg.warning[sec] = []*portToIP{value} } else { @@ -65,7 +94,7 @@ func (csg *groupedSecurityGroups) addToWarning(sec *securityGroup, value *portTo } } -func (csg *groupedSecurityGroups) addToUnknown(sec *securityGroup, value *portToIP) { +func (csg *groupedSecurityGroups) addToUnknown(sec *SecurityGroup, value *portToIP) { if _, ok := csg.unknown[sec]; !ok { csg.unknown[sec] = []*portToIP{value} } else { @@ -73,7 +102,7 @@ func (csg *groupedSecurityGroups) addToUnknown(sec *securityGroup, value *portTo } } -func (csg *groupedSecurityGroups) addToWideOpen(sec *securityGroup, value *portToIP) { +func (csg *groupedSecurityGroups) addToWideOpen(sec *SecurityGroup, value *portToIP) { if _, ok := csg.wideOpen[sec]; !ok { csg.wideOpen[sec] = []*portToIP{value} } else { @@ -81,7 +110,7 @@ func (csg *groupedSecurityGroups) addToWideOpen(sec *securityGroup, value *portT } } -func (csg *groupedSecurityGroups) addToAmazon(sec *securityGroup, value *portToIP) { +func (csg *groupedSecurityGroups) addToAmazon(sec *SecurityGroup, value *portToIP) { if _, ok := csg.amazon[sec]; !ok { csg.amazon[sec] = []*portToIP{value} } else { @@ -89,7 +118,7 @@ func (csg *groupedSecurityGroups) addToAmazon(sec *securityGroup, value *portToI } } -func (csg *groupedSecurityGroups) addToSG(sec *securityGroup, value *portToIP) { +func (csg *groupedSecurityGroups) addToSG(sec *SecurityGroup, value *portToIP) { if _, ok := csg.sg[sec]; !ok { csg.sg[sec] = []*portToIP{value} } else { @@ -123,6 +152,7 @@ func (g *groupedIPBlockRules) addToAlert(cidr *net.IPNet) { g.alert = append(g.alert, cidr) } +// AWSIPRanges is the JSON struct used to parse the AWS IP Range file. type AWSIPRanges struct { SyncToken string `json:"syncToken"` CreateDate string `json:"createDate"` @@ -130,6 +160,7 @@ type AWSIPRanges struct { IPv6Prefixes []*IPv6Prefix `json:"ipv6_prefixes"` } +// Prefix is used with AWSIPRanges. type Prefix struct { IPPrefix string `json:"ip_prefix"` Region string `json:"region"` @@ -137,6 +168,7 @@ type Prefix struct { Service string `json:"service"` } +// GetCIDR will extract the IPPrefix as a CIDR definition. func (p *Prefix) GetCIDR() (*net.IPNet, error) { _, ipv4Net, err := net.ParseCIDR(p.IPPrefix) if err != nil { @@ -145,6 +177,7 @@ func (p *Prefix) GetCIDR() (*net.IPNet, error) { return ipv4Net, err } +// GetService will extract the AWS service name that the IP is associated with. func (p *Prefix) GetService() string { switch p.Service { case "AMAZON": @@ -154,6 +187,7 @@ func (p *Prefix) GetService() string { } } +// IPv6Prefix is used with AWSIPRanges. type IPv6Prefix struct { IPv6Prefix string `json:"ipv6_prefix"` Region string `json:"region"` @@ -161,6 +195,7 @@ type IPv6Prefix struct { Service string `json:"service"` } +// GetCIDR will extract the IPPrefix as a CIDR definition. func (p *IPv6Prefix) GetCIDR() (*net.IPNet, error) { _, ipv6Net, err := net.ParseCIDR(p.IPv6Prefix) if err != nil { @@ -169,6 +204,7 @@ func (p *IPv6Prefix) GetCIDR() (*net.IPNet, error) { return ipv6Net, err } +// AWSIPs is a type AWSIPs struct { list []*net.IPNet table map[*net.IPNet]*Prefix diff --git a/sg/types_test.go b/sg/types_test.go new file mode 100644 index 0000000..51268b9 --- /dev/null +++ b/sg/types_test.go @@ -0,0 +1,83 @@ +package sg + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSecurityGroup_ID(t *testing.T) { + is := assert.New(t) + + t.Run("should return the literal string", func(t *testing.T) { + test := "sg-1234" + sg := SecurityGroup{ + id: test, + } + + is.Equal(test, sg.ID()) + }) + + t.Run("should return empty string when there is no value", func(t *testing.T) { + sg := SecurityGroup{} + + is.Equal("", sg.ID()) + }) +} + +func TestSecurityGroup_Name(t *testing.T) { + is := assert.New(t) + + t.Run("should return the literal string", func(t *testing.T) { + test := "test-name" + sg := SecurityGroup{ + name: test, + } + + is.Equal(test, sg.Name()) + }) + + t.Run("should return empty string when there is no value", func(t *testing.T) { + sg := SecurityGroup{} + + is.Equal("", sg.Name()) + }) +} + +func TestSecurityGroup_Attachments(t *testing.T) { + is := assert.New(t) + + t.Run("should return the literal string", func(t *testing.T) { + attachments := map[string]int{"ec2": 1, "rds": 2} + sg := SecurityGroup{ + attached: attachments, + } + + is.Equal(attachments, sg.Attachments()) + }) + + t.Run("should return empty string when there is no value", func(t *testing.T) { + sg := SecurityGroup{} + + var test map[string]int + is.Equal(test, sg.Attachments()) + }) +} + +func TestSecurityGroup_GetAttachmentsAsString(t *testing.T) { + is := assert.New(t) + + t.Run("should return the literal string", func(t *testing.T) { + attachments := map[string]int{"ec2": 1, "rds": 2} + sg := SecurityGroup{ + attached: attachments, + } + + is.Equal("ec2: 1 rds: 2", sg.GetAttachmentsAsString()) + }) + + t.Run("should return empty string when there is no value", func(t *testing.T) { + sg := SecurityGroup{} + + is.Equal("", sg.GetAttachmentsAsString()) + }) +}