├── .gitignore ├── main.go ├── csasession ├── clientfactory │ ├── factory.go │ ├── kmsclient.go │ ├── iamclient.go │ ├── ec2client.go │ ├── s3client.go │ ├── mocks │ │ ├── clientfactory_mock.go │ │ ├── kmsclient_mock.go │ │ ├── iamclient_mock.go │ │ ├── ec2client_mock.go │ │ └── s3client_mock.go │ └── api.go ├── sessionfactory │ ├── factory.go │ └── api.go └── session.go ├── report ├── volumereport.go ├── sortabletags.go ├── sortabletags_test.go ├── iam_checklist_report.go ├── report.go ├── iamreport.go ├── s3report_test.go ├── ec2report_test.go ├── ec2report.go └── s3report.go ├── configuration ├── config_mocks.go ├── config.go └── mfa.go ├── environment ├── helpers.go ├── createfiles.go └── checkfiles.go ├── resource ├── resource_test.go ├── volumes_test.go ├── securitygroups_test.go ├── ec2s_test.go ├── images_test.go ├── snapshots_test.go ├── ec2s.go ├── volumes.go ├── snapshots.go ├── securitygroups.go ├── images.go ├── resource.go ├── kmskeys_test.go ├── iam.go ├── bucketpolicy.go ├── s3_test.go ├── kmskeys.go └── s3.go ├── Makefile ├── scanner └── scanner.go ├── .circleci └── config.yml ├── logger └── logger.go ├── cmd └── root.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | cloud-security-audit.log 3 | debug.test 4 | cloud-security-audit 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /csasession/clientfactory/factory.go: -------------------------------------------------------------------------------- 1 | package clientfactory 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/csasession/sessionfactory" 5 | ) 6 | 7 | // ClientFactory provides methods for creation and management of service clients. 8 | type ClientFactoryAWS struct { 9 | sessionFactory *sessionfactory.SessionFactory 10 | } 11 | 12 | // New creates a new instance of the ClientFactory. 13 | func New(sessionFactory *sessionfactory.SessionFactory) ClientFactory { 14 | factory := &ClientFactoryAWS{ 15 | sessionFactory: sessionFactory, 16 | } 17 | 18 | return factory 19 | } 20 | -------------------------------------------------------------------------------- /csasession/sessionfactory/factory.go: -------------------------------------------------------------------------------- 1 | package sessionfactory 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | ) 8 | 9 | // sessionFactory provides methods for creation and management of service clients and sessions. 10 | type SessionFactory struct { 11 | regionToSession map[string]*session.Session 12 | mutex sync.Mutex 13 | } 14 | 15 | // New creates a new instance of the sessionFactory. 16 | func New() *SessionFactory { 17 | factory := &SessionFactory{ 18 | regionToSession: make(map[string]*session.Session), 19 | } 20 | 21 | return factory 22 | } 23 | -------------------------------------------------------------------------------- /report/volumereport.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | type VolumeReport []string 9 | 10 | func (v *VolumeReport) AddEBS(volumeID string, encryptionType EncryptionType) { 11 | *v = append(*v, volumeID+fmt.Sprintf("[%s]", encryptionType.String())) 12 | } 13 | 14 | func (v *VolumeReport) ToTableData() string { 15 | if len(*v) == 0 { 16 | return "" 17 | } 18 | var buffer bytes.Buffer 19 | n := len(*v) 20 | for _, volumeID := range (*v)[:n-1] { 21 | buffer.WriteString(volumeID + "\n") 22 | } 23 | buffer.WriteString((*v)[n-1]) 24 | return buffer.String() 25 | } 26 | -------------------------------------------------------------------------------- /configuration/config_mocks.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 5 | "github.com/Appliscale/cloud-security-audit/csasession/sessionfactory" 6 | "github.com/Appliscale/cloud-security-audit/logger" 7 | "testing" 8 | ) 9 | 10 | func GetTestConfig(t *testing.T) (config Config) { 11 | myLogger := logger.CreateQuietLogger() 12 | config.Logger = &myLogger 13 | clientFactory := mocks.NewClientFactoryMock(t) 14 | config.ClientFactory = &clientFactory 15 | config.SessionFactory = sessionfactory.New() 16 | 17 | return config 18 | } 19 | -------------------------------------------------------------------------------- /environment/helpers.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "os/user" 5 | ) 6 | 7 | func GetUserHomeDir() (string, error) { 8 | myUser, userError := user.Current() 9 | if userError != nil { 10 | return "", userError 11 | } 12 | path := myUser.HomeDir 13 | 14 | return path, nil 15 | } 16 | 17 | func UniqueNonEmptyElementsOf(s []string) []string { 18 | unique := make(map[string]bool, len(s)) 19 | us := make([]string, len(unique)) 20 | for _, elem := range s { 21 | if len(elem) != 0 { 22 | if !unique[elem] { 23 | us = append(us, elem) 24 | unique[elem] = true 25 | } 26 | } 27 | } 28 | return us 29 | } 30 | -------------------------------------------------------------------------------- /resource/resource_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "testing" 5 | 6 | "errors" 7 | "github.com/Appliscale/cloud-security-audit/configuration" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type MockResource struct { 12 | DummyProperty string 13 | } 14 | 15 | func (m *MockResource) LoadFromAWS(config *configuration.Config, region string) error { 16 | return errors.New("Dummy error") 17 | } 18 | 19 | func TestResourcesReturnsErrorFromLoadFromAWSFunc(t *testing.T) { 20 | mockResource := &MockResource{} 21 | config := configuration.GetTestConfig(t) 22 | assert.Error(t, LoadResources(&config, "region", mockResource)) 23 | } 24 | -------------------------------------------------------------------------------- /csasession/clientfactory/kmsclient.go: -------------------------------------------------------------------------------- 1 | package clientfactory 2 | 3 | import "github.com/aws/aws-sdk-go/service/kms" 4 | 5 | type KmsClient interface { 6 | ListKeys(input *kms.ListKeysInput) (*kms.ListKeysOutput, error) 7 | ListAliases(input *kms.ListAliasesInput) (*kms.ListAliasesOutput, error) 8 | } 9 | 10 | type AWSKmsClient struct { 11 | api *kms.KMS 12 | } 13 | 14 | func (client AWSKmsClient) ListKeys(input *kms.ListKeysInput) (*kms.ListKeysOutput, error) { 15 | return client.api.ListKeys(input) 16 | } 17 | func (client AWSKmsClient) ListAliases(input *kms.ListAliasesInput) (*kms.ListAliasesOutput, error) { 18 | return client.api.ListAliases(input) 19 | } 20 | -------------------------------------------------------------------------------- /csasession/clientfactory/iamclient.go: -------------------------------------------------------------------------------- 1 | package clientfactory 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/iam" 5 | ) 6 | 7 | type IAMClient interface { 8 | ListUsers(input *iam.GetAccountAuthorizationDetailsInput) (*iam.GetAccountAuthorizationDetailsOutput, error) 9 | ListAccessKeys(input *iam.ListAccessKeysInput) (*iam.ListAccessKeysOutput, error) 10 | } 11 | 12 | type AWSIAMClient struct { 13 | api *iam.IAM 14 | } 15 | 16 | func (client AWSIAMClient) ListUsers(input *iam.GetAccountAuthorizationDetailsInput) (*iam.GetAccountAuthorizationDetailsOutput, error) { 17 | return client.api.GetAccountAuthorizationDetails(input) 18 | } 19 | 20 | func (client AWSIAMClient) ListAccessKeys(input *iam.ListAccessKeysInput) (*iam.ListAccessKeysOutput, error) { 21 | return client.api.ListAccessKeys(input) 22 | } 23 | -------------------------------------------------------------------------------- /resource/volumes_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | "testing" 9 | ) 10 | 11 | func TestLoadVolumesFromAWS(t *testing.T) { 12 | config := configuration.GetTestConfig(t) 13 | defer config.ClientFactory.(*mocks.ClientFactoryMock).Destroy() 14 | 15 | ec2Client, _ := config.ClientFactory.GetEc2Client(csasession.SessionConfig{}) 16 | ec2Client.(*mocks.MockEC2Client). 17 | EXPECT(). 18 | DescribeVolumes(&ec2.DescribeVolumesInput{}). 19 | Times(1). 20 | Return(&ec2.DescribeVolumesOutput{}, nil) 21 | 22 | LoadResource(&Volumes{}, &config, "region") 23 | } 24 | -------------------------------------------------------------------------------- /configuration/config.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory" 5 | "github.com/Appliscale/cloud-security-audit/csasession/sessionfactory" 6 | "github.com/Appliscale/cloud-security-audit/logger" 7 | ) 8 | 9 | type Config struct { 10 | Regions *[]string 11 | Services *[]string 12 | Profile string 13 | SessionFactory *sessionfactory.SessionFactory 14 | ClientFactory clientfactory.ClientFactory 15 | Logger *logger.Logger 16 | Mfa bool 17 | MfaDuration int64 18 | } 19 | 20 | func GetConfig() (config Config) { 21 | myLogger := logger.CreateDefaultLogger() 22 | config.Logger = &myLogger 23 | config.SessionFactory = sessionfactory.New() 24 | config.ClientFactory = clientfactory.New(config.SessionFactory) 25 | 26 | return config 27 | } 28 | -------------------------------------------------------------------------------- /resource/securitygroups_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | "testing" 9 | ) 10 | 11 | func TestLoadSecurityGroupsFromAWS(t *testing.T) { 12 | config := configuration.GetTestConfig(t) 13 | defer config.ClientFactory.(*mocks.ClientFactoryMock).Destroy() 14 | 15 | ec2Client, _ := config.ClientFactory.GetEc2Client(csasession.SessionConfig{}) 16 | ec2Client.(*mocks.MockEC2Client). 17 | EXPECT(). 18 | DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{}). 19 | Times(1). 20 | Return(&ec2.DescribeSecurityGroupsOutput{}, nil) 21 | 22 | LoadResource(&SecurityGroups{}, &config, "region") 23 | } 24 | -------------------------------------------------------------------------------- /resource/ec2s_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | "testing" 9 | ) 10 | 11 | func TestLoadEC2sFromAWS(t *testing.T) { 12 | config := configuration.GetTestConfig(t) 13 | defer config.ClientFactory.(*mocks.ClientFactoryMock).Destroy() 14 | 15 | ec2Client, _ := config.ClientFactory.GetEc2Client(csasession.SessionConfig{}) 16 | ec2Client.(*mocks.MockEC2Client). 17 | EXPECT(). 18 | DescribeInstances(&ec2.DescribeInstancesInput{}). 19 | Times(1). 20 | Return(&ec2.DescribeInstancesOutput{ 21 | Reservations: []*ec2.Reservation{}, 22 | NextToken: nil, 23 | }, nil) 24 | 25 | LoadResource(&Ec2s{}, &config, "region") 26 | } 27 | -------------------------------------------------------------------------------- /resource/images_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | "testing" 10 | ) 11 | 12 | func TestLoadImagesFromAWS(t *testing.T) { 13 | config := configuration.GetTestConfig(t) 14 | defer config.ClientFactory.(*mocks.ClientFactoryMock).Destroy() 15 | 16 | ec2Client, _ := config.ClientFactory.GetEc2Client(csasession.SessionConfig{}) 17 | ec2Client.(*mocks.MockEC2Client). 18 | EXPECT(). 19 | DescribeImages(&ec2.DescribeImagesInput{ 20 | Owners: []*string{aws.String("self")}, 21 | }). 22 | Times(1). 23 | Return(&ec2.DescribeImagesOutput{}, nil) 24 | 25 | LoadResource(&Images{}, &config, "region") 26 | } 27 | -------------------------------------------------------------------------------- /resource/snapshots_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | "testing" 10 | ) 11 | 12 | func TestLoadSnapshotsFromAWS(t *testing.T) { 13 | config := configuration.GetTestConfig(t) 14 | defer config.ClientFactory.(*mocks.ClientFactoryMock).Destroy() 15 | 16 | ec2Client, _ := config.ClientFactory.GetEc2Client(csasession.SessionConfig{}) 17 | ec2Client.(*mocks.MockEC2Client). 18 | EXPECT(). 19 | DescribeSnapshots(&ec2.DescribeSnapshotsInput{ 20 | OwnerIds: []*string{aws.String("self")}, 21 | }). 22 | Times(1). 23 | Return(&ec2.DescribeSnapshotsOutput{}, nil) 24 | 25 | LoadResource(&Snapshots{}, &config, "region") 26 | } 27 | -------------------------------------------------------------------------------- /csasession/session.go: -------------------------------------------------------------------------------- 1 | package csasession 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | ) 7 | 8 | // SessionConfig provides session configuration for clients. 9 | type SessionConfig struct { 10 | Profile string 11 | Region string 12 | } 13 | 14 | // CreateSession returns new AWS session. 15 | func CreateSession(config SessionConfig) (*session.Session, error) { 16 | 17 | sess, err := session.NewSessionWithOptions( 18 | session.Options{ 19 | Config: aws.Config{ 20 | Region: &config.Region, 21 | }, 22 | Profile: config.Profile, 23 | }) 24 | 25 | return sess, err 26 | } 27 | 28 | // GetAvailableRegions returns list of available regions. 29 | func GetAvailableRegions() *[]string { 30 | return &[]string{ 31 | "us-east-2", 32 | "us-east-1", 33 | "us-west-1", 34 | "us-west-2", 35 | "ap-northeast-1", 36 | "ap-northeast-2", 37 | "ap-south-1", 38 | "ap-southeast-1", 39 | "ap-southeast-2", 40 | "ca-central-1", 41 | "eu-central-1", 42 | "eu-west-1", 43 | "eu-west-2", 44 | "eu-west-3", 45 | "sa-east-1", 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHONY: get-deps code-analysis test all 2 | 3 | all: get-deps code-analysis test 4 | 5 | get-deps: 6 | go get -t -v ./... 7 | go install ./... 8 | go build 9 | go fmt ./... 10 | 11 | code-analysis: get-deps 12 | go vet -v ./... 13 | 14 | test: get-deps create-mocks 15 | go test -cover ./... 16 | 17 | 18 | create-mocks: get-mockgen 19 | `go env GOPATH`/bin/mockgen -source=./csasession/clientfactory/ec2client.go -destination=./csasession/clientfactory/mocks/ec2client_mock.go -package=mocks EC2Client 20 | `go env GOPATH`/bin/mockgen -source=./csasession/clientfactory/kmsclient.go -destination=./csasession/clientfactory/mocks/kmsclient_mock.go -package=mocks KmsClient 21 | `go env GOPATH`/bin/mockgen -source=./csasession/clientfactory/s3client.go -destination=./csasession/clientfactory/mocks/s3client_mock.go -package=mocks S3Client 22 | `go env GOPATH`/bin/mockgen -source=./csasession/clientfactory/iamclient.go -destination=./csasession/clientfactory/mocks/iamclient_mock.go -package=mocks IAMClient 23 | 24 | get-mockgen: 25 | go get github.com/golang/mock/gomock 26 | go install github.com/golang/mock/mockgen 27 | -------------------------------------------------------------------------------- /resource/ec2s.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/aws/aws-sdk-go/aws/awserr" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | ) 9 | 10 | type Ec2s []*ec2.Instance 11 | 12 | func (e *Ec2s) LoadFromAWS(config *configuration.Config, region string) error { 13 | ec2API, err := config.ClientFactory.GetEc2Client(csasession.SessionConfig{Profile: config.Profile, Region: region}) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | q := &ec2.DescribeInstancesInput{} 19 | for { 20 | result, err := ec2API.DescribeInstances(q) 21 | if err != nil { 22 | if aerr, ok := err.(awserr.Error); ok { 23 | switch aerr.Code() { 24 | case "OptInRequired": 25 | config.Logger.Warning("you are not subscribed to the EC2 service in region: " + region) 26 | break 27 | default: 28 | return err 29 | } 30 | } else { 31 | return err 32 | } 33 | } 34 | for _, reservation := range result.Reservations { 35 | *e = append(*e, reservation.Instances...) 36 | } 37 | if result.NextToken == nil { 38 | break 39 | } 40 | q.NextToken = result.NextToken 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /resource/volumes.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/aws/aws-sdk-go/aws/awserr" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | ) 9 | 10 | type Volumes []*ec2.Volume 11 | 12 | func (v *Volumes) LoadFromAWS(config *configuration.Config, region string) error { 13 | ec2API, err := config.ClientFactory.GetEc2Client(csasession.SessionConfig{Profile: config.Profile, Region: region}) 14 | if err != nil { 15 | return err 16 | } 17 | q := &ec2.DescribeVolumesInput{} 18 | for { 19 | result, err := ec2API.DescribeVolumes(q) 20 | if err != nil { 21 | if aerr, ok := err.(awserr.Error); ok { 22 | switch aerr.Code() { 23 | case "OptInRequired": 24 | break 25 | default: 26 | return err 27 | } 28 | } else { 29 | return err 30 | } 31 | } 32 | *v = append(*v, result.Volumes...) 33 | if result.NextToken == nil { 34 | break 35 | } 36 | q.NextToken = result.NextToken 37 | 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (v *Volumes) FindById(id string) *ec2.Volume { 44 | for _, volume := range *v { 45 | if *volume.VolumeId == id { 46 | return volume 47 | } 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /report/sortabletags.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | ) 10 | 11 | type SortableTags struct { 12 | Tags map[string]string 13 | Keys []string 14 | } 15 | 16 | func NewSortableTags() *SortableTags { 17 | return &SortableTags{Tags: make(map[string]string)} 18 | } 19 | 20 | func (st *SortableTags) Add(tags []*ec2.Tag) { 21 | 22 | for _, tag := range tags { 23 | st.Keys = append(st.Keys, *tag.Key) 24 | st.Tags[*tag.Key] = *tag.Value 25 | } 26 | less := func(i, j int) bool { 27 | return strings.ToLower(st.Keys[i]) < strings.ToLower(st.Keys[j]) 28 | } 29 | sort.Slice(st.Keys, less) 30 | } 31 | 32 | func (st *SortableTags) ToTableData() string { 33 | n := len(st.Keys) 34 | if n == 0 { 35 | return "" 36 | } 37 | var buffer bytes.Buffer 38 | for _, key := range st.Keys[:n-1] { 39 | maxWidth := 50 40 | if len(st.Tags[key]+key) > maxWidth-1 { 41 | i := maxWidth - 1 - len(key) 42 | for i < len(st.Tags[key]) { 43 | st.Tags[key] = st.Tags[key][:i] + "\n " + st.Tags[key][i:] 44 | i += maxWidth - 2 45 | } 46 | } 47 | buffer.WriteString(key + ":" + st.Tags[key] + "\n") 48 | } 49 | buffer.WriteString(st.Keys[n-1] + ":" + st.Tags[st.Keys[n-1]]) 50 | return buffer.String() 51 | } 52 | -------------------------------------------------------------------------------- /resource/snapshots.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/awserr" 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | ) 10 | 11 | type Snapshots []*ec2.Snapshot 12 | 13 | func (s *Snapshots) LoadFromAWS(config *configuration.Config, region string) error { 14 | ec2API, err := config.ClientFactory.GetEc2Client(csasession.SessionConfig{Profile: config.Profile, Region: region}) 15 | if err != nil { 16 | return err 17 | } 18 | q := &ec2.DescribeSnapshotsInput{ 19 | OwnerIds: []*string{aws.String("self")}, 20 | } 21 | for { 22 | result, err := ec2API.DescribeSnapshots(q) 23 | if err != nil { 24 | if aerr, ok := err.(awserr.Error); ok { 25 | switch aerr.Code() { 26 | case "OptInRequired": 27 | break 28 | default: 29 | return err 30 | } 31 | } else { 32 | return err 33 | } 34 | } 35 | *s = append(*s, result.Snapshots...) 36 | if result.NextToken == nil { 37 | break 38 | } 39 | q.NextToken = result.NextToken 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (s *Snapshots) FindById(id string) *ec2.Snapshot { 46 | for _, snapshot := range *s { 47 | if id == *snapshot.SnapshotId { 48 | return snapshot 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /resource/securitygroups.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/aws/aws-sdk-go/aws/awserr" 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | ) 9 | 10 | type SecurityGroups map[string][]*ec2.IpPermission 11 | 12 | func (s *SecurityGroups) LoadFromAWS(config *configuration.Config, region string) error { 13 | 14 | ec2API, err := config.ClientFactory.GetEc2Client(csasession.SessionConfig{Profile: config.Profile, Region: region}) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | q := &ec2.DescribeSecurityGroupsInput{} 20 | 21 | for { 22 | result, err := ec2API.DescribeSecurityGroups(q) 23 | if err != nil { 24 | if aerr, ok := err.(awserr.Error); ok { 25 | switch aerr.Code() { 26 | case "OptInRequired": 27 | break 28 | default: 29 | return err 30 | } 31 | } else { 32 | return err 33 | } 34 | } 35 | for _, sg := range result.SecurityGroups { 36 | (*s)[*sg.GroupId] = append((*s)[*sg.GroupId], sg.IpPermissions...) 37 | } 38 | if result.NextToken == nil { 39 | break 40 | } 41 | q.NextToken = result.NextToken 42 | } 43 | return nil 44 | } 45 | 46 | func (s *SecurityGroups) GetIpPermissionsByID(groupID string) []*ec2.IpPermission { 47 | 48 | if sg, ok := (*s)[groupID]; ok { 49 | return sg 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /report/sortabletags_test.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestIfTagsAreSortedCorrectly(t *testing.T) { 13 | 14 | st := NewSortableTags() 15 | ec2Tags := []*ec2.Tag{ 16 | { 17 | Key: aws.String("BBBB"), 18 | Value: aws.String("SomeValue6"), 19 | }, 20 | { 21 | Key: aws.String("bbb"), 22 | Value: aws.String("SomeValue2"), 23 | }, 24 | { 25 | Key: aws.String("AAA"), 26 | Value: aws.String("SomeValue3"), 27 | }, 28 | { 29 | Key: aws.String("aaaa"), 30 | Value: aws.String("SomeValue4"), 31 | }, 32 | } 33 | st.Add(ec2Tags) 34 | tableData := strings.Split(st.ToTableData(), "\n") 35 | assert.True(t, len(ec2Tags) == len(tableData)) 36 | assert.EqualValues(t, "AAA", strings.Split(tableData[0], ":")[0]) 37 | assert.EqualValues(t, "aaaa", strings.Split(tableData[1], ":")[0]) 38 | assert.EqualValues(t, "bbb", strings.Split(tableData[2], ":")[0]) 39 | assert.EqualValues(t, "BBBB", strings.Split(tableData[3], ":")[0]) 40 | } 41 | 42 | func TestSorttableTagsIfSliceIsEmptyDoNotPanic(t *testing.T) { 43 | st := NewSortableTags() 44 | ec2Tags := []*ec2.Tag{} 45 | st.Add(ec2Tags) 46 | 47 | defer func() { 48 | if r := recover(); r != nil { 49 | t.Errorf("The code did panic") 50 | } 51 | }() 52 | 53 | st.ToTableData() 54 | } 55 | -------------------------------------------------------------------------------- /csasession/sessionfactory/api.go: -------------------------------------------------------------------------------- 1 | package sessionfactory 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/csasession" 5 | 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/s3" 8 | ) 9 | 10 | // GetSession returns cached AWS session. 11 | func (factory *SessionFactory) GetSession(config csasession.SessionConfig) (*session.Session, error) { 12 | factory.mutex.Lock() 13 | defer factory.mutex.Unlock() 14 | 15 | if sess, ok := factory.regionToSession[config.Region]; ok { 16 | return sess, nil 17 | } 18 | 19 | return factory.NewSession(config) 20 | } 21 | 22 | // NewSession creates a new session and caches it. 23 | func (factory *SessionFactory) NewSession(config csasession.SessionConfig) (*session.Session, error) { 24 | sess, err := csasession.CreateSession(config) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | factory.regionToSession[config.Region] = sess 30 | return sess, nil 31 | } 32 | 33 | func (factory *SessionFactory) SetNormalizeBucketLocation(config csasession.SessionConfig) error { 34 | sess, err := factory.GetSession(config) 35 | if err != nil { 36 | return err 37 | } 38 | sess.Handlers.Unmarshal.PushBackNamed(s3.NormalizeBucketLocationHandler) 39 | return nil 40 | } 41 | 42 | func (factory *SessionFactory) ReinitialiseSession(config csasession.SessionConfig) (err error) { 43 | factory.mutex.Lock() 44 | defer factory.mutex.Unlock() 45 | 46 | _, err = factory.NewSession(config) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /resource/images.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/Appliscale/cloud-security-audit/configuration" 7 | "github.com/Appliscale/cloud-security-audit/csasession" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/awserr" 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | ) 12 | 13 | type Images []*ec2.Image 14 | 15 | func (im Images) SortByDate() { 16 | sort.SliceStable(im, func(i, j int) bool { 17 | return *(im)[i].CreationDate < *(im)[j].CreationDate 18 | }) 19 | } 20 | 21 | func (im *Images) FindByTags(tags map[string]string) Images { 22 | found := Images{} 23 | n := 0 24 | for _, image := range *im { 25 | for _, tag := range image.Tags { 26 | if v, ok := tags[*tag.Key]; ok && v == *tag.Value { 27 | n++ 28 | if len(tags) == n { 29 | found = append(found, image) 30 | } 31 | } 32 | } 33 | n = 0 34 | } 35 | return found 36 | } 37 | 38 | func (im *Images) LoadFromAWS(config *configuration.Config, region string) error { 39 | ec2API, err := config.ClientFactory.GetEc2Client(csasession.SessionConfig{Profile: config.Profile, Region: region}) 40 | if err != nil { 41 | return err 42 | } 43 | result, err := ec2API.DescribeImages(&ec2.DescribeImagesInput{ 44 | Owners: []*string{aws.String("self")}, 45 | }) 46 | if err != nil { 47 | if aerr, ok := err.(awserr.Error); ok { 48 | switch aerr.Code() { 49 | case "OptInRequired": 50 | break 51 | default: 52 | return err 53 | } 54 | } else { 55 | return err 56 | } 57 | } 58 | *im = result.Images 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Appliscale/cloud-security-audit/configuration" 8 | "github.com/Appliscale/cloud-security-audit/report" 9 | ) 10 | 11 | func Run(config *configuration.Config) error { 12 | 13 | for _, service := range *config.Services { 14 | switch strings.ToLower(service) { 15 | case "ec2": 16 | config.Logger.Info("Gathering information about EC2s...") 17 | ec2Reports := report.Ec2Reports{} 18 | resources, err := ec2Reports.GetResources(config) 19 | if err != nil { 20 | return err 21 | } 22 | ec2Reports.GenerateReport(resources) 23 | report.PrintTable(&ec2Reports) 24 | case "s3": 25 | config.Logger.Info("Gathering information about S3s...") 26 | s3BucketReports := report.S3BucketReports{} 27 | resources, err := s3BucketReports.GetResources(config) 28 | if err != nil { 29 | return err 30 | } 31 | s3BucketReports.GenerateReport(resources) 32 | report.PrintTable(&s3BucketReports) 33 | case "iam": 34 | config.Logger.Info("Gathering information about IAM...") 35 | iamReports := report.IAMReports{} 36 | resources, err := iamReports.GetResources(config) 37 | if err != nil { 38 | return err 39 | } 40 | iamReports.GenerateReport(resources) 41 | report.PrintTable(&iamReports) 42 | 43 | iamChecklist := report.IAMChecklist{} 44 | iamChecklist.GenerateReport(resources) 45 | report.PrintTable(&iamChecklist) 46 | 47 | default: 48 | return fmt.Errorf("Wrong service name: %s", service) 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /csasession/clientfactory/ec2client.go: -------------------------------------------------------------------------------- 1 | package clientfactory 2 | 3 | import "github.com/aws/aws-sdk-go/service/ec2" 4 | 5 | type EC2Client interface { 6 | DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) 7 | DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error) 8 | DescribeSecurityGroups(input *ec2.DescribeSecurityGroupsInput) (*ec2.DescribeSecurityGroupsOutput, error) 9 | DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error) 10 | DescribeSnapshots(input *ec2.DescribeSnapshotsInput) (*ec2.DescribeSnapshotsOutput, error) 11 | } 12 | 13 | type AWSEC2Client struct { 14 | api *ec2.EC2 15 | } 16 | 17 | func (client AWSEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { 18 | return client.api.DescribeInstances(input) 19 | } 20 | func (client AWSEC2Client) DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error) { 21 | return client.api.DescribeVolumes(input) 22 | } 23 | func (client AWSEC2Client) DescribeSecurityGroups(input *ec2.DescribeSecurityGroupsInput) (*ec2.DescribeSecurityGroupsOutput, error) { 24 | return client.api.DescribeSecurityGroups(input) 25 | } 26 | 27 | func (client AWSEC2Client) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error) { 28 | return client.api.DescribeImages(input) 29 | } 30 | 31 | func (client AWSEC2Client) DescribeSnapshots(input *ec2.DescribeSnapshotsInput) (*ec2.DescribeSnapshotsOutput, error) { 32 | return client.api.DescribeSnapshots(input) 33 | } 34 | -------------------------------------------------------------------------------- /resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "sync" 7 | 8 | "github.com/Appliscale/cloud-security-audit/configuration" 9 | ) 10 | 11 | type Resource interface { 12 | LoadFromAWS(config *configuration.Config, region string) error 13 | } 14 | 15 | func LoadResource(r Resource, config *configuration.Config, region string) error { 16 | err := r.LoadFromAWS(config, region) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func LoadResources(config *configuration.Config, region string, resources ...Resource) error { 25 | 26 | var wg sync.WaitGroup 27 | n := len(resources) 28 | wg.Add(n) 29 | errs := make(chan error, n) 30 | 31 | go func() { 32 | wg.Wait() 33 | close(errs) 34 | }() 35 | 36 | for _, r := range resources { 37 | go func(r Resource) { 38 | defer wg.Done() 39 | errs <- r.LoadFromAWS(config, region) 40 | }(r) 41 | } 42 | 43 | for err := range errs { 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func SaveToFile(r Resource, filename string) error { 53 | 54 | b, err := json.MarshalIndent(r, "", " ") 55 | if err != nil { 56 | return err 57 | } 58 | return ioutil.WriteFile(filename, b, 0644) 59 | } 60 | 61 | func LoadFromFile(r Resource, filename string) error { 62 | 63 | bytes, err := ioutil.ReadFile(filename) 64 | if err != nil { 65 | return err 66 | } 67 | return json.Unmarshal(bytes, r) 68 | } 69 | 70 | func GetAvailableServices() *[]string { 71 | return &[]string{ 72 | "ec2", 73 | "s3", 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /report/iam_checklist_report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Appliscale/cloud-security-audit/configuration" 6 | "github.com/Appliscale/cloud-security-audit/resource" 7 | //"github.com/aws/aws-sdk-go/service/iam" 8 | ) 9 | 10 | type IAMItem struct { 11 | Name string 12 | Value bool 13 | } 14 | 15 | func NewIAMItem(name string, value bool) *IAMItem { 16 | return &IAMItem{ 17 | name, 18 | value, 19 | } 20 | } 21 | 22 | type IAMChecklist []*IAMItem 23 | 24 | type IAMChecklistRequiredResources struct { 25 | IAMInfo *resource.IAMInfo 26 | } 27 | 28 | func (i *IAMChecklist) GetHeaders() []string { 29 | return []string{"Guideline", "Status"} 30 | } 31 | 32 | func (i *IAMChecklist) FormatDataToTable() [][]string { 33 | data := [][]string{} 34 | 35 | for _, iamItem := range *i { 36 | row := []string{ 37 | iamItem.Name, 38 | fmt.Sprintf("%t", iamItem.Value), 39 | } 40 | data = append(data, row) 41 | } 42 | 43 | return data 44 | } 45 | 46 | func (i *IAMChecklist) GenerateReport(r *IAMReportRequiredResources) { 47 | 48 | *i = append(*i, NewIAMItem("Root account's access keys locked away", !r.IAMInfo.HasRootAccessKeys())) 49 | 50 | } 51 | 52 | func (i *IAMChecklist) GetResources(config *configuration.Config) (*IAMReportRequiredResources, error) { 53 | resources := &IAMReportRequiredResources{IAMInfo: &resource.IAMInfo{}} 54 | 55 | for _, region := range *config.Regions { 56 | err := resource.LoadResources( 57 | config, 58 | region, 59 | resources.IAMInfo, 60 | ) 61 | 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | 67 | return resources, nil 68 | } 69 | -------------------------------------------------------------------------------- /resource/kmskeys_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/kms" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestKMSKeysloadValuesToMapSetCustomToTrueWhenAliasAWSIsNotPresent(t *testing.T) { 12 | prefix := "arn:aws:kms:us-east-1:123456789101:" 13 | aliasName := "alias/some-alias" 14 | keyID := "abcdefgh-1234-5678-1234-abcdefghijkl" 15 | keyArn := prefix + "key/" + keyID 16 | aliases := &KMSKeyAliases{ 17 | &kms.AliasListEntry{ 18 | TargetKeyId: &keyID, 19 | AliasArn: aws.String(prefix + aliasName), 20 | AliasName: &aliasName, 21 | }, 22 | } 23 | keyListEntries := &KMSKeysListEntries{ 24 | &kms.KeyListEntry{ 25 | KeyArn: &keyArn, 26 | KeyId: &keyID, 27 | }, 28 | } 29 | kmsKeys := NewKMSKeys() 30 | kmsKeys.loadValuesToMap(aliases, keyListEntries) 31 | assert.True(t, kmsKeys.Values[keyArn].Custom) 32 | } 33 | 34 | func TestKMSKeysloadValuesToMapSetCustomToFalseWhenAliasAWSIsPresent(t *testing.T) { 35 | prefix := "arn:aws:kms:us-east-1:123456789101:" 36 | aliasName := "alias/aws/ebs" 37 | keyID := "abcdefgh-1234-5678-1234-abcdefghijkl" 38 | keyArn := prefix + "key/" + keyID 39 | aliases := &KMSKeyAliases{ 40 | &kms.AliasListEntry{ 41 | TargetKeyId: &keyID, 42 | AliasArn: aws.String(prefix + aliasName), 43 | AliasName: &aliasName, 44 | }, 45 | } 46 | keyListEntries := &KMSKeysListEntries{ 47 | &kms.KeyListEntry{ 48 | KeyArn: &keyArn, 49 | KeyId: &keyID, 50 | }, 51 | } 52 | kmsKeys := NewKMSKeys() 53 | kmsKeys.loadValuesToMap(aliases, keyListEntries) 54 | assert.False(t, kmsKeys.Values[keyArn].Custom) 55 | } 56 | -------------------------------------------------------------------------------- /csasession/clientfactory/s3client.go: -------------------------------------------------------------------------------- 1 | package clientfactory 2 | 3 | import "github.com/aws/aws-sdk-go/service/s3" 4 | 5 | type S3Client interface { 6 | GetBucketPolicy(input *s3.GetBucketPolicyInput) (*s3.GetBucketPolicyOutput, error) 7 | GetBucketEncryption(input *s3.GetBucketEncryptionInput) (*s3.GetBucketEncryptionOutput, error) 8 | GetBucketLogging(input *s3.GetBucketLoggingInput) (*s3.GetBucketLoggingOutput, error) 9 | GetBucketAcl(input *s3.GetBucketAclInput) (*s3.GetBucketAclOutput, error) 10 | ListBuckets(input *s3.ListBucketsInput) (*s3.ListBucketsOutput, error) 11 | GetBucketLocation(input *s3.GetBucketLocationInput) (*s3.GetBucketLocationOutput, error) 12 | } 13 | 14 | type AWSS3Client struct { 15 | api *s3.S3 16 | } 17 | 18 | func (client AWSS3Client) GetBucketPolicy(input *s3.GetBucketPolicyInput) (*s3.GetBucketPolicyOutput, error) { 19 | return client.api.GetBucketPolicy(input) 20 | } 21 | func (client AWSS3Client) GetBucketEncryption(input *s3.GetBucketEncryptionInput) (*s3.GetBucketEncryptionOutput, error) { 22 | return client.api.GetBucketEncryption(input) 23 | } 24 | func (client AWSS3Client) GetBucketLogging(input *s3.GetBucketLoggingInput) (*s3.GetBucketLoggingOutput, error) { 25 | return client.api.GetBucketLogging(input) 26 | } 27 | func (client AWSS3Client) GetBucketAcl(input *s3.GetBucketAclInput) (*s3.GetBucketAclOutput, error) { 28 | return client.api.GetBucketAcl(input) 29 | } 30 | func (client AWSS3Client) ListBuckets(input *s3.ListBucketsInput) (*s3.ListBucketsOutput, error) { 31 | return client.api.ListBuckets(input) 32 | } 33 | 34 | func (client AWSS3Client) GetBucketLocation(input *s3.GetBucketLocationInput) (*s3.GetBucketLocationOutput, error) { 35 | return client.api.GetBucketLocation(input) 36 | } 37 | -------------------------------------------------------------------------------- /csasession/clientfactory/mocks/clientfactory_mock.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/csasession" 5 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory" 6 | "github.com/golang/mock/gomock" 7 | "testing" 8 | ) 9 | 10 | type ClientFactoryMock struct { 11 | mockCtrl *gomock.Controller 12 | kmsClient *MockKmsClient 13 | ec2Client *MockEC2Client 14 | s3Client *MockS3Client 15 | iamClient *MockIAMClient 16 | } 17 | 18 | func NewClientFactoryMock(t *testing.T) ClientFactoryMock { 19 | clientMock := ClientFactoryMock{ 20 | mockCtrl: gomock.NewController(t), 21 | } 22 | return clientMock 23 | } 24 | 25 | func (client *ClientFactoryMock) GetKmsClient(config csasession.SessionConfig) (clientfactory.KmsClient, error) { 26 | if client.kmsClient == nil { 27 | client.kmsClient = NewMockKmsClient(client.mockCtrl) 28 | } 29 | return client.kmsClient, nil 30 | } 31 | func (client *ClientFactoryMock) GetEc2Client(config csasession.SessionConfig) (clientfactory.EC2Client, error) { 32 | if client.ec2Client == nil { 33 | client.ec2Client = NewMockEC2Client(client.mockCtrl) 34 | } 35 | return client.ec2Client, nil 36 | } 37 | func (client *ClientFactoryMock) GetS3Client(config csasession.SessionConfig) (clientfactory.S3Client, error) { 38 | if client.s3Client == nil { 39 | client.s3Client = NewMockS3Client(client.mockCtrl) 40 | } 41 | return client.s3Client, nil 42 | } 43 | func (client *ClientFactoryMock) GetIAMClient(config csasession.SessionConfig) (clientfactory.IAMClient, error) { 44 | if client.iamClient == nil { 45 | client.iamClient = NewMockIAMClient(client.mockCtrl) 46 | } 47 | return client.iamClient, nil 48 | } 49 | 50 | func (client *ClientFactoryMock) Destroy() { 51 | client.mockCtrl.Finish() 52 | } 53 | -------------------------------------------------------------------------------- /resource/iam.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/configuration" 5 | "github.com/Appliscale/cloud-security-audit/csasession" 6 | "github.com/aws/aws-sdk-go/aws/awserr" 7 | "github.com/aws/aws-sdk-go/service/iam" 8 | ) 9 | 10 | type Users []*iam.UserDetail 11 | 12 | type IAMInfo struct { 13 | hasRootAccessKeys bool 14 | users Users 15 | } 16 | 17 | func (iaminfo IAMInfo) GetUsers() Users { 18 | return iaminfo.users 19 | } 20 | 21 | func (iaminfo IAMInfo) HasRootAccessKeys() bool { 22 | return iaminfo.hasRootAccessKeys 23 | } 24 | 25 | func (iaminfo *IAMInfo) LoadFromAWS(config *configuration.Config, region string) error { 26 | iamAPI, err := config.ClientFactory.GetIAMClient(csasession.SessionConfig{Profile: config.Profile, Region: region}) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | q := &iam.GetAccountAuthorizationDetailsInput{} 32 | 33 | // get Users 34 | result, err := iamAPI.ListUsers(q) 35 | CheckError(region, config, err) 36 | (*iaminfo).users = result.UserDetailList 37 | 38 | //get access keys for root 39 | username := "root" 40 | userInput := &iam.ListAccessKeysInput{UserName: &username} 41 | accesskeys, err := iamAPI.ListAccessKeys(userInput) 42 | 43 | //check if root has access keys 44 | (iaminfo).hasRootAccessKeys = len(accesskeys.AccessKeyMetadata) > 0 45 | 46 | return nil 47 | } 48 | 49 | func CheckError(region string, config *configuration.Config, err error) error { 50 | if err != nil { 51 | if aerr, ok := err.(awserr.Error); ok { 52 | switch aerr.Code() { 53 | case "OptInRequired": 54 | config.Logger.Warning("you are not subscribed to the IAM service in region: " + region) 55 | break 56 | default: 57 | return err 58 | } 59 | } else { 60 | return err 61 | } 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "github.com/olekukonko/tablewriter" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | type Report interface { 10 | FormatDataToTable() [][]string 11 | GetHeaders() []string 12 | } 13 | 14 | func PrintTable(r Report) { 15 | 16 | data := r.FormatDataToTable() 17 | 18 | table := tablewriter.NewWriter(os.Stdout) 19 | // Configure Headers 20 | table.SetReflowDuringAutoWrap(false) 21 | table.SetAutoFormatHeaders(false) 22 | table.SetHeader(customFormatHeaders(r.GetHeaders())) 23 | // Configure rows&cells 24 | table.SetRowSeparator("-") 25 | table.SetRowLine(true) 26 | table.SetAutoWrapText(false) 27 | table.AppendBulk(data) 28 | table.Render() 29 | } 30 | 31 | func customFormatHeaders(headers []string) []string { 32 | for i, header := range headers { 33 | headers[i] = Title(header) 34 | } 35 | return headers 36 | } 37 | 38 | func Title(name string) string { 39 | origLen := len(name) 40 | name = strings.Replace(name, "_", " ", -1) 41 | //name = strings.Replace(name, ".", " ", -1) 42 | name = strings.TrimSpace(name) 43 | if len(name) == 0 && origLen > 0 { 44 | // Keep at least one character. This is important to preserve 45 | // empty lines in multi-line headers/footers. 46 | name = " " 47 | } 48 | return strings.ToUpper(name) 49 | } 50 | 51 | type EncryptionType int 52 | 53 | const ( 54 | // NONE : No Encryption 55 | NONE EncryptionType = 0 56 | // AES256 : 256-bit Advanced Encryption Standard 57 | AES256 EncryptionType = 1 58 | // DKMS : Encrypted with default KMS Key 59 | DKMS EncryptionType = 2 60 | // CKMS : Encrypted with Custom KMS Key 61 | CKMS EncryptionType = 3 62 | ) 63 | 64 | func (et EncryptionType) String() string { 65 | types := [...]string{"NONE", "AES256", "DKMS", "CKMS"} 66 | if et < NONE || et > CKMS { 67 | return "Unknown" 68 | } 69 | return types[et] 70 | } 71 | -------------------------------------------------------------------------------- /csasession/clientfactory/api.go: -------------------------------------------------------------------------------- 1 | package clientfactory 2 | 3 | import ( 4 | "github.com/Appliscale/cloud-security-audit/csasession" 5 | "github.com/aws/aws-sdk-go/service/iam" 6 | 7 | "github.com/aws/aws-sdk-go/service/ec2" 8 | "github.com/aws/aws-sdk-go/service/kms" 9 | "github.com/aws/aws-sdk-go/service/s3" 10 | ) 11 | 12 | type ClientFactory interface { 13 | GetKmsClient(config csasession.SessionConfig) (KmsClient, error) 14 | GetEc2Client(config csasession.SessionConfig) (EC2Client, error) 15 | GetS3Client(config csasession.SessionConfig) (S3Client, error) 16 | GetIAMClient(config csasession.SessionConfig) (IAMClient, error) 17 | } 18 | 19 | // GetKmsClient creates a new KMS client from cached session. 20 | func (factory *ClientFactoryAWS) GetKmsClient(config csasession.SessionConfig) (KmsClient, error) { 21 | sess, err := factory.sessionFactory.GetSession(config) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | client := kms.New(sess) 27 | return AWSKmsClient{api: client}, nil 28 | } 29 | 30 | // GetEc2Client creates a new EC2 client from cached session. 31 | func (factory *ClientFactoryAWS) GetEc2Client(config csasession.SessionConfig) (EC2Client, error) { 32 | sess, err := factory.sessionFactory.GetSession(config) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | client := ec2.New(sess) 38 | return AWSEC2Client{api: client}, nil 39 | } 40 | 41 | // GetS3Client creates a new S3 client from cached session. 42 | func (factory *ClientFactoryAWS) GetS3Client(config csasession.SessionConfig) (S3Client, error) { 43 | sess, err := factory.sessionFactory.GetSession(config) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | client := s3.New(sess) 49 | return AWSS3Client{api: client}, nil 50 | } 51 | 52 | // GetIAMClient creates a new IAM client from cached session. 53 | func (factory *ClientFactoryAWS) GetIAMClient(config csasession.SessionConfig) (IAMClient, error) { 54 | sess, err := factory.sessionFactory.GetSession(config) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | client := iam.New(sess) 60 | return AWSIAMClient{api: client}, nil 61 | } 62 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.9 6 | working_directory: /go/src/github.com/Appliscale/cloud-security-audit 7 | steps: 8 | - checkout 9 | - run: make get-deps 10 | - run: make code-analysis 11 | - run: make test 12 | 13 | release: 14 | docker: 15 | - image: circleci/golang:1.9 16 | working_directory: /go/src/github.com/Appliscale/cloud-security-audit 17 | steps: 18 | - checkout 19 | - run: make 20 | - run: mkdir -p release 21 | - run: 22 | name: "Build Cloud Security Audit for Linux" 23 | command: | 24 | GOOS=linux GOARCH=amd64 go build -o release/cloud-security-audit-linux-amd64 25 | tar -C release -czf release/cloud-security-audit-linux-amd64.tar.gz cloud-security-audit-linux-amd64 26 | - run: 27 | name: "Build Cloud Security Audit for Darwin" 28 | command: | 29 | GOOS=darwin GOARCH=amd64 go build -o release/cloud-security-audit-darwin-amd64 30 | tar -C release -czf release/cloud-security-audit-darwin-amd64.tar.gz cloud-security-audit-darwin-amd64 31 | - run: 32 | name: "Get gothub" 33 | command: | 34 | go get github.com/itchio/gothub 35 | - run: 36 | name: "Publish release on github" 37 | command: | 38 | gothub upload --user ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} --tag ${CIRCLE_TAG} --name "cloud-security-audit-darwin-amd64" --file release/cloud-security-audit-darwin-amd64.tar.gz 39 | gothub upload --user ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} --tag ${CIRCLE_TAG} --name "cloud-security-audit-linux-amd64" --file release/cloud-security-audit-linux-amd64.tar.gz 40 | 41 | 42 | workflows: 43 | version: 2 44 | main: 45 | jobs: 46 | - build: 47 | filters: 48 | tags: 49 | only: /^v\d+\.\d+(\.\d+)?$/ 50 | - release: 51 | requires: 52 | - build 53 | filters: 54 | branches: 55 | ignore: /.*/ 56 | tags: 57 | only: /^v\d+\.\d+(\.\d+)?$/ 58 | -------------------------------------------------------------------------------- /csasession/clientfactory/mocks/kmsclient_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./csasession/clientfactory/kmsclient.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | kms "github.com/aws/aws-sdk-go/service/kms" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockKmsClient is a mock of KmsClient interface 14 | type MockKmsClient struct { 15 | ctrl *gomock.Controller 16 | recorder *MockKmsClientMockRecorder 17 | } 18 | 19 | // MockKmsClientMockRecorder is the mock recorder for MockKmsClient 20 | type MockKmsClientMockRecorder struct { 21 | mock *MockKmsClient 22 | } 23 | 24 | // NewMockKmsClient creates a new mock instance 25 | func NewMockKmsClient(ctrl *gomock.Controller) *MockKmsClient { 26 | mock := &MockKmsClient{ctrl: ctrl} 27 | mock.recorder = &MockKmsClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockKmsClient) EXPECT() *MockKmsClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // ListKeys mocks base method 37 | func (m *MockKmsClient) ListKeys(input *kms.ListKeysInput) (*kms.ListKeysOutput, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "ListKeys", input) 40 | ret0, _ := ret[0].(*kms.ListKeysOutput) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // ListKeys indicates an expected call of ListKeys 46 | func (mr *MockKmsClientMockRecorder) ListKeys(input interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKeys", reflect.TypeOf((*MockKmsClient)(nil).ListKeys), input) 49 | } 50 | 51 | // ListAliases mocks base method 52 | func (m *MockKmsClient) ListAliases(input *kms.ListAliasesInput) (*kms.ListAliasesOutput, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "ListAliases", input) 55 | ret0, _ := ret[0].(*kms.ListAliasesOutput) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // ListAliases indicates an expected call of ListAliases 61 | func (mr *MockKmsClientMockRecorder) ListAliases(input interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAliases", reflect.TypeOf((*MockKmsClient)(nil).ListAliases), input) 64 | } 65 | -------------------------------------------------------------------------------- /csasession/clientfactory/mocks/iamclient_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./csasession/clientfactory/iamclient.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | iam "github.com/aws/aws-sdk-go/service/iam" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockIAMClient is a mock of IAMClient interface 14 | type MockIAMClient struct { 15 | ctrl *gomock.Controller 16 | recorder *MockIAMClientMockRecorder 17 | } 18 | 19 | // MockIAMClientMockRecorder is the mock recorder for MockIAMClient 20 | type MockIAMClientMockRecorder struct { 21 | mock *MockIAMClient 22 | } 23 | 24 | // NewMockIAMClient creates a new mock instance 25 | func NewMockIAMClient(ctrl *gomock.Controller) *MockIAMClient { 26 | mock := &MockIAMClient{ctrl: ctrl} 27 | mock.recorder = &MockIAMClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockIAMClient) EXPECT() *MockIAMClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // ListUsers mocks base method 37 | func (m *MockIAMClient) ListUsers(input *iam.GetAccountAuthorizationDetailsInput) (*iam.GetAccountAuthorizationDetailsOutput, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "ListUsers", input) 40 | ret0, _ := ret[0].(*iam.GetAccountAuthorizationDetailsOutput) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // ListUsers indicates an expected call of ListUsers 46 | func (mr *MockIAMClientMockRecorder) ListUsers(input interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockIAMClient)(nil).ListUsers), input) 49 | } 50 | 51 | // ListAccessKeys mocks base method 52 | func (m *MockIAMClient) ListAccessKeys(input *iam.ListAccessKeysInput) (*iam.ListAccessKeysOutput, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "ListAccessKeys", input) 55 | ret0, _ := ret[0].(*iam.ListAccessKeysOutput) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // ListAccessKeys indicates an expected call of ListAccessKeys 61 | func (mr *MockIAMClientMockRecorder) ListAccessKeys(input interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccessKeys", reflect.TypeOf((*MockIAMClient)(nil).ListAccessKeys), input) 64 | } 65 | -------------------------------------------------------------------------------- /report/iamreport.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Appliscale/cloud-security-audit/configuration" 6 | "github.com/Appliscale/cloud-security-audit/resource" 7 | "github.com/aws/aws-sdk-go/service/iam" 8 | "strings" 9 | ) 10 | 11 | type IAMReport struct { 12 | UserName string 13 | Groups string 14 | InlinePolicies int 15 | } 16 | 17 | func NewIAMReport(u iam.UserDetail) *IAMReport { 18 | 19 | return &IAMReport{ 20 | getUserName(u), 21 | getUserGroups(u), 22 | getNoOfUserInlinePolicies(u), 23 | } 24 | } 25 | 26 | type IAMReports []*IAMReport 27 | 28 | type IAMReportRequiredResources struct { 29 | IAMInfo *resource.IAMInfo 30 | } 31 | 32 | func (i *IAMReports) GetHeaders() []string { 33 | return []string{"User name", "Groups", "# of Inline\npolicies"} 34 | } 35 | 36 | func (i *IAMReports) FormatDataToTable() [][]string { 37 | data := [][]string{} 38 | 39 | for _, iamReport := range *i { 40 | row := []string{ 41 | iamReport.UserName, 42 | iamReport.Groups, 43 | fmt.Sprintf("%d", iamReport.InlinePolicies), 44 | } 45 | data = append(data, row) 46 | } 47 | 48 | return data 49 | } 50 | 51 | func (i *IAMReports) GenerateReport(r *IAMReportRequiredResources) { 52 | for _, user := range (*r.IAMInfo).GetUsers() { 53 | iamReport := NewIAMReport(*user) 54 | *i = append(*i, iamReport) 55 | } 56 | 57 | } 58 | 59 | func (i *IAMReports) GetResources(config *configuration.Config) (*IAMReportRequiredResources, error) { 60 | resources := &IAMReportRequiredResources{IAMInfo: &resource.IAMInfo{}} 61 | 62 | for _, region := range *config.Regions { 63 | err := resource.LoadResources( 64 | config, 65 | region, 66 | resources.IAMInfo, 67 | ) 68 | 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | return resources, nil 75 | } 76 | 77 | func getUserName(u iam.UserDetail) string { 78 | return *u.UserName 79 | } 80 | 81 | func getUserGroups(u iam.UserDetail) string { 82 | var grouplist []string 83 | if len(u.GroupList) != 0 { 84 | for _, group := range u.GroupList { 85 | grouplist = append(grouplist, *group) 86 | } 87 | } 88 | 89 | groups := "None" 90 | 91 | if len(grouplist) > 0 { 92 | groups = strings.Join(grouplist, ",") 93 | } 94 | 95 | return groups 96 | } 97 | 98 | func getNoOfUserInlinePolicies(u iam.UserDetail) int { 99 | return len(u.UserPolicyList) 100 | } 101 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Logger struct { 9 | Quiet bool 10 | Yes bool 11 | Verbosity Verbosity 12 | } 13 | 14 | type ResourceValidation struct { 15 | ResourceName string 16 | Errors []string 17 | } 18 | 19 | type Verbosity int 20 | 21 | const ( 22 | TRACE Verbosity = iota 23 | DEBUG 24 | INFO 25 | ERROR 26 | WARNING 27 | ) 28 | 29 | var verboseModes = [...]string{ 30 | "TRACE", 31 | "DEBUG", 32 | "INFO", 33 | "ERROR", 34 | "WARNING", 35 | } 36 | 37 | func (verbosity Verbosity) String() string { 38 | return verboseModes[verbosity] 39 | } 40 | 41 | // Create default logger. 42 | func CreateDefaultLogger() Logger { 43 | return Logger{ 44 | Quiet: false, 45 | Yes: false, 46 | Verbosity: INFO, 47 | } 48 | } 49 | 50 | // Create quiet logger. 51 | func CreateQuietLogger() Logger { 52 | return Logger{ 53 | Quiet: true, 54 | Yes: false, 55 | Verbosity: INFO, 56 | } 57 | } 58 | 59 | // Log always - no matter the verbosity level. 60 | func (logger *Logger) Always(message string) { 61 | fmt.Println(message) 62 | } 63 | 64 | // Log error. 65 | func (logger *Logger) Warning(warning string) { 66 | logger.log(WARNING, warning) 67 | } 68 | 69 | // Log error. 70 | func (logger *Logger) Error(err string) { 71 | logger.log(ERROR, err) 72 | } 73 | 74 | // Log info. 75 | func (logger *Logger) Info(info string) { 76 | logger.log(INFO, info) 77 | } 78 | 79 | // Log debug. 80 | func (logger *Logger) Debug(debug string) { 81 | logger.log(DEBUG, debug) 82 | } 83 | 84 | // Log trace. 85 | func (logger *Logger) Trace(trace string) { 86 | logger.log(TRACE, trace) 87 | } 88 | 89 | // Get input from command line. 90 | func (logger *Logger) GetInput(message string, v ...interface{}) error { 91 | fmt.Printf("%s: ", message) 92 | _, err := fmt.Scanln(v...) 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | func (logger *Logger) log(verbosity Verbosity, message string) { 99 | if !logger.Quiet && verbosity >= logger.Verbosity { 100 | fmt.Println(verbosity.String() + ": " + message) 101 | } 102 | } 103 | 104 | // Set logger verbosity. 105 | func (logger *Logger) SetVerbosity(verbosity string) { 106 | for index, element := range verboseModes { 107 | if strings.ToUpper(verbosity) == element { 108 | logger.Verbosity = Verbosity(index) 109 | } 110 | } 111 | } 112 | 113 | func IsVerbosityValid(verbosity string) bool { 114 | switch verbosity { 115 | case 116 | "TRACE", 117 | "DEBUG", 118 | "INFO", 119 | "WARNING", 120 | "ERROR": 121 | return true 122 | } 123 | return false 124 | } 125 | -------------------------------------------------------------------------------- /report/s3report_test.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/Appliscale/cloud-security-audit/resource" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/s3" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type Permissions struct { 14 | in string 15 | out string 16 | } 17 | 18 | func TestS3Report_WhenSSEAlgorithmIsAES256CheckEncryptionTypeReturnsAES256(t *testing.T) { 19 | 20 | s3BucketReport := &S3BucketReport{} 21 | AES256Rule := s3.ServerSideEncryptionByDefault{ 22 | SSEAlgorithm: aws.String("AES256"), 23 | } 24 | s3BucketReport.CheckEncryptionType(AES256Rule, resource.NewKMSKeys()) 25 | assert.Equal(t, AES256, s3BucketReport.EncryptionType) 26 | } 27 | 28 | func TestS3Report_WhenSSEAlgorithmIsCustomAWSKMSCheckEncryptionTypeReturnsCKMS(t *testing.T) { 29 | kmsKeyArn := "arn:aws:kms:us-east-1:126286021559:key/2fdaec7f-6f04-4b2c-b6ea-a1a6d8437c3e" 30 | kmsKeys := resource.NewKMSKeys() 31 | kmsKeys.Values[kmsKeyArn] = &resource.KMSKey{ 32 | Custom: true, 33 | } 34 | 35 | s3BucketReport := &S3BucketReport{} 36 | customKMSKeyRule := s3.ServerSideEncryptionByDefault{ 37 | KMSMasterKeyID: &kmsKeyArn, 38 | SSEAlgorithm: aws.String("aws:kms"), 39 | } 40 | 41 | s3BucketReport.CheckEncryptionType(customKMSKeyRule, kmsKeys) 42 | assert.Equalf(t, CKMS, s3BucketReport.EncryptionType, fmt.Sprintf("Expected %s, got %s", CKMS.String(), s3BucketReport.EncryptionType)) 43 | } 44 | 45 | func TestS3Report_WhenSSEAlgorithmIsDefaultAWSKMSCheckEncryptionTypeReturnsDKMS(t *testing.T) { 46 | kmsKeyArn := "arn:aws:kms:us-east-1:126286021559:key/2fdaec7f-6f04-4b2c-b6ea-a1a6d8437c3e" 47 | kmsKeys := resource.NewKMSKeys() 48 | kmsKeys.Values[kmsKeyArn] = &resource.KMSKey{ 49 | Custom: false, 50 | } 51 | s3BucketReport := &S3BucketReport{} 52 | customKMSKeyRule := s3.ServerSideEncryptionByDefault{ 53 | KMSMasterKeyID: &kmsKeyArn, 54 | SSEAlgorithm: aws.String("aws:kms"), 55 | } 56 | s3BucketReport.CheckEncryptionType(customKMSKeyRule, kmsKeys) 57 | assert.Equal(t, DKMS, s3BucketReport.EncryptionType) 58 | } 59 | 60 | func TestGetTypeOfAccessACL(t *testing.T) { 61 | var permissions = []Permissions{ 62 | {"WRITE_ACP", "W"}, 63 | {"READ", "R"}, 64 | {"DELETE", "D"}, 65 | {"FULL_CONTROL", "RWD"}, 66 | } 67 | 68 | for _, permission := range permissions { 69 | got := getTypeOfAccessACL(permission.in) 70 | assert.Equalf(t, got, permission.out, fmt.Sprintf("Expected %s, got %s", permission.out, got)) 71 | } 72 | 73 | } 74 | 75 | func TestGetTypeOfAccessPolicy(t *testing.T) { 76 | var actions = resource.Actions{"s3:DeleteObject", "s3:GetObjectVersion", "s3:PutObjectAcl"} 77 | got := getTypeOfAccessPolicy(actions) 78 | expected := "[DRW]" 79 | assert.Equalf(t, got, expected, fmt.Sprintf("Expected %s, got %s", expected, got)) 80 | 81 | } 82 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Appliscale/cloud-security-audit/configuration" 7 | "github.com/Appliscale/cloud-security-audit/csasession" 8 | "github.com/Appliscale/cloud-security-audit/resource" 9 | 10 | "github.com/Appliscale/cloud-security-audit/environment" 11 | "github.com/Appliscale/cloud-security-audit/scanner" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // var cfgFile string 16 | var config = configuration.GetConfig() 17 | 18 | // rootCmd represents the base command when called without any subcommands 19 | var rootCmd = &cobra.Command{ 20 | Use: "cloud-security-audit", 21 | Short: "Scan for vulnerabilities in your AWS Account.", 22 | Long: `Scan for vulnerabilities in your AWS Account.`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if environment.CheckAWSConfigFiles(&config) { 25 | err := scanner.Run(&config) 26 | if err != nil { 27 | config.Logger.Error(err.Error()) 28 | } 29 | } 30 | }, 31 | } 32 | 33 | // Execute adds all child commands to the root command and sets flags appropriately. 34 | // This is called by main.main(). It only needs to happen once to the rootCmd. 35 | func Execute() { 36 | if err := rootCmd.Execute(); err != nil { 37 | config.Logger.Error(err.Error()) 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | var ( 43 | region string 44 | service string 45 | profile string 46 | mfa bool 47 | mfaDuration int64 48 | ) 49 | 50 | func init() { 51 | cobra.OnInitialize(initConfig) 52 | 53 | rootCmd.Flags().StringVarP(®ion, "region", "r", "", "specify aws region to scan your account,e.g. --region us-east-1") 54 | 55 | rootCmd.Flags().StringVarP(&service, "service", "s", "", "specify aws service to scan in your account,e.g. --service [ec2:x,ec2:image]") 56 | 57 | rootCmd.Flags().StringVarP(&profile, "profile", "p", "", "specify aws profile e.g. --profile appliscale") 58 | 59 | rootCmd.Flags().BoolVarP(&mfa, "mfa", "m", false, "indicates usage of Multi Factor Authentication") 60 | rootCmd.Flags().Int64VarP(&mfaDuration, "mfa-duration", "d", 0, "sets the duration of the MFA session") 61 | } 62 | 63 | func getRegions() *[]string { 64 | if region != "" { 65 | return &[]string{region} 66 | } 67 | 68 | return csasession.GetAvailableRegions() 69 | } 70 | 71 | func getServices() *[]string { 72 | if service != "" { 73 | return &[]string{service} 74 | } 75 | 76 | return resource.GetAvailableServices() 77 | } 78 | 79 | func getProfile() string { 80 | if profile != "" { 81 | return profile 82 | } 83 | 84 | if profile, ok := os.LookupEnv("AWS_PROFILE"); ok { 85 | return profile 86 | } 87 | 88 | return "default" 89 | } 90 | 91 | func initConfig() { 92 | config.Regions = getRegions() 93 | config.Services = getServices() 94 | config.Profile = getProfile() 95 | config.Mfa = mfa 96 | config.MfaDuration = mfaDuration 97 | configuration.InitialiseMFA(config) 98 | } 99 | -------------------------------------------------------------------------------- /resource/bucketpolicy.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | type S3Policy struct { 9 | Version string 10 | Id string `json:",omitempty"` 11 | Statements []Statement `json:"Statement"` 12 | } 13 | 14 | func NewS3Policy(s string) (*S3Policy, error) { 15 | b := []byte(s) 16 | s3Policy := &S3Policy{} 17 | err := json.Unmarshal(b, s3Policy) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return s3Policy, nil 22 | } 23 | 24 | type Statement struct { 25 | Effect string 26 | Principal Principal `json:"Principal"` 27 | Actions Actions `json:"Action"` 28 | Resource Resources `json:"Resource"` 29 | Condition Condition `json:",omitempty"` 30 | } 31 | 32 | type Condition struct { 33 | Bool map[string]string `json:",omitempty"` 34 | Null map[string]string `json:",omitempty"` 35 | } 36 | 37 | type Actions []string 38 | 39 | func (a *Actions) UnmarshalJSON(b []byte) error { 40 | 41 | array := []string{} 42 | err := json.Unmarshal(b, &array) 43 | /* 44 | if error is: "json: cannot unmarshal string into Go value of type []string" 45 | then fallback to unmarshaling string 46 | */ 47 | if err != nil { 48 | s := "" 49 | err = json.Unmarshal(b, &s) 50 | if err != nil { 51 | return err 52 | } 53 | *a = append(*a, s) 54 | return nil 55 | } 56 | for _, action := range array { 57 | *a = append(*a, action) 58 | } 59 | return nil 60 | } 61 | 62 | // Principal : Specifies user, account, service or other 63 | // entity that is allowed or denied access to resource 64 | type Principal struct { 65 | Map map[string][]string // Values in Map: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html 66 | Wildcard string // Values: * 67 | } 68 | 69 | func (p *Principal) UnmarshalJSON(b []byte) error { 70 | p.Map = make(map[string][]string) 71 | s := "" 72 | err := json.Unmarshal(b, &s) 73 | if err != nil { 74 | m := make(map[string]interface{}) 75 | 76 | err = json.Unmarshal(b, &m) 77 | if err != nil { 78 | return err 79 | } 80 | for key, value := range m { 81 | switch t := value.(type) { 82 | case string: 83 | p.Map[key] = append(p.Map[key], value.(string)) 84 | case []interface{}: 85 | for _, elem := range value.([]interface{}) { 86 | p.Map[key] = append(p.Map[key], elem.(string)) 87 | } 88 | default: 89 | return errors.New("Unsupported type " + t.(string)) 90 | 91 | } 92 | } 93 | } 94 | p.Wildcard = s 95 | return nil 96 | } 97 | 98 | type Resources []string 99 | 100 | func (r *Resources) UnmarshalJSON(b []byte) error { 101 | 102 | array := []string{} 103 | err := json.Unmarshal(b, &array) 104 | /* 105 | if error is: "json: cannot unmarshal string into Go value of type []string" 106 | then fallback to unmarshaling string 107 | */ 108 | if err != nil { 109 | s := "" 110 | err = json.Unmarshal(b, &s) 111 | if err != nil { 112 | return err 113 | } 114 | *r = append(*r, s) 115 | return nil 116 | } 117 | for _, resource := range array { 118 | *r = append(*r, resource) 119 | } 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /report/ec2report_test.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/Appliscale/cloud-security-audit/resource" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | 12 | "github.com/aws/aws-sdk-go/service/ec2" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestEC2Report_IfVolumeEncryptedWithDefaultKMSLineHasSuffixDKMS(t *testing.T) { 17 | kmsKeyArn := "arn:aws:kms:us-east-1:123456789101:key/abcdefgh-1234-5678-1234-abcdefghijkl" 18 | volumeID := "vol-111" 19 | volume := &ec2.Volume{ 20 | VolumeId: &volumeID, 21 | Encrypted: aws.Bool(true), 22 | KmsKeyId: &kmsKeyArn, 23 | } 24 | instanceBlockDeviceMapping := &ec2.InstanceBlockDeviceMapping{ 25 | Ebs: &ec2.EbsInstanceBlockDevice{ 26 | VolumeId: &volumeID, 27 | }, 28 | } 29 | instance := &ec2.Instance{ 30 | InstanceId: &volumeID, 31 | BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{instanceBlockDeviceMapping}, 32 | Placement: &ec2.Placement{ 33 | AvailabilityZone: aws.String("us-east-1a"), 34 | }, 35 | } 36 | kmsKeys := resource.NewKMSKeys() 37 | kmsKeys.Values[kmsKeyArn] = &resource.KMSKey{Custom: false} 38 | r := &Ec2ReportRequiredResources{ 39 | Ec2s: &resource.Ec2s{instance}, 40 | Volumes: &resource.Volumes{volume}, 41 | KMSKeys: kmsKeys, 42 | AvailabilityZone: "Zone", 43 | } 44 | ec2Reports := &Ec2Reports{} 45 | ec2Reports.GenerateReport(r) 46 | 47 | assert.Equal(t, volumeID+"[DKMS]", (*(*ec2Reports)[0].VolumeReport)[0]) 48 | } 49 | 50 | func TestEC2Report_IfVolumeNotEncryptedLineHasSuffixNone(t *testing.T) { 51 | 52 | volumeID := "vol-111" 53 | volume := &ec2.Volume{ 54 | VolumeId: &volumeID, 55 | Encrypted: aws.Bool(false), 56 | } 57 | instanceBlockDeviceMapping := &ec2.InstanceBlockDeviceMapping{ 58 | Ebs: &ec2.EbsInstanceBlockDevice{ 59 | VolumeId: &volumeID, 60 | }, 61 | } 62 | instance := &ec2.Instance{ 63 | InstanceId: &volumeID, 64 | BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{instanceBlockDeviceMapping}, 65 | Placement: &ec2.Placement{ 66 | AvailabilityZone: aws.String("us-east-1a"), 67 | }, 68 | } 69 | r := &Ec2ReportRequiredResources{ 70 | Ec2s: &resource.Ec2s{instance}, 71 | Volumes: &resource.Volumes{volume}, 72 | KMSKeys: resource.NewKMSKeys(), 73 | } 74 | ec2Reports := &Ec2Reports{} 75 | ec2Reports.GenerateReport(r) 76 | 77 | assert.Equal(t, volumeID+"[NONE]", (*(*ec2Reports)[0].VolumeReport)[0]) 78 | } 79 | 80 | func TestEC2Report_IfTagsInTableDataAreFormattedCorrectly(t *testing.T) { 81 | 82 | instanceID := "i-1" 83 | ec2Tags := []*ec2.Tag{ 84 | { 85 | Key: aws.String("key1"), 86 | Value: aws.String("value"), 87 | }, 88 | { 89 | Key: aws.String("key2"), 90 | Value: aws.String("some value2"), 91 | }, 92 | } 93 | 94 | ec2Report1 := NewEc2Report(instanceID) 95 | ec2Report1.SortableTags.Add(ec2Tags) 96 | 97 | ec2Reports := Ec2Reports{ec2Report1} 98 | tableData := ec2Reports.FormatDataToTable() 99 | 100 | formattedTags := strings.Split(tableData[0][4], "\n") 101 | sort.Strings(formattedTags) 102 | 103 | assert.True(t, len(formattedTags) == 2) 104 | for i, tag := range ec2Tags { 105 | assert.EqualValues(t, *tag.Key+":"+*tag.Value, formattedTags[i]) 106 | } 107 | 108 | } 109 | 110 | func TestVolumeIfSliceIsEmptyDoNotPanic(t *testing.T) { 111 | v := &VolumeReport{} 112 | 113 | defer func() { 114 | if r := recover(); r != nil { 115 | t.Errorf("The code did panic") 116 | } 117 | }() 118 | 119 | v.ToTableData() 120 | } 121 | -------------------------------------------------------------------------------- /environment/createfiles.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Appliscale/cloud-security-audit/configuration" 6 | "github.com/Appliscale/perun/helpers" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func CreateAWSCredentialsFile(config *configuration.Config, profile string) { 12 | if profile != "" { 13 | config.Logger.Always("You haven't got .aws/credentials file for profile " + profile) 14 | var awsAccessKeyID string 15 | var awsSecretAccessKey string 16 | var mfaSerial string 17 | 18 | config.Logger.GetInput("awsAccessKeyID", &awsAccessKeyID) 19 | config.Logger.GetInput("awsSecretAccessKey", &awsSecretAccessKey) 20 | config.Logger.GetInput("mfaSerial", &mfaSerial) 21 | 22 | homePath, pathError := GetUserHomeDir() 23 | if pathError != nil { 24 | config.Logger.Error(pathError.Error()) 25 | } 26 | path := homePath + "/.aws/credentials" 27 | line := "\n[" + profile + "-long-term" + "]\n" 28 | appendStringToFile(path, line) 29 | line = "aws_access_key_id" + " = " + awsAccessKeyID + "\n" 30 | appendStringToFile(path, line) 31 | line = "aws_secret_access_key" + " = " + awsSecretAccessKey + "\n" 32 | appendStringToFile(path, line) 33 | line = "mfa_serial" + " = " + mfaSerial + "\n" 34 | appendStringToFile(path, line) 35 | } 36 | } 37 | 38 | func CreateAWSConfigFile(config *configuration.Config, profile string, region string, output string) { 39 | if output == "" { 40 | output = getUserOutput(config) 41 | } 42 | homePath, pathError := GetUserHomeDir() 43 | if pathError != nil { 44 | config.Logger.Error(pathError.Error()) 45 | } 46 | path := homePath + "/.aws/config" 47 | line := "\n[" + profile + "]\n" 48 | appendStringToFile(path, line) 49 | line = "region" + " = " + region + "\n" 50 | appendStringToFile(path, line) 51 | line = "output" + " = " + output + "\n" 52 | appendStringToFile(path, line) 53 | } 54 | 55 | func createConfigProfileFromCredentials(homeDir string, config *configuration.Config, profile string) { 56 | profilesInCredentials := UniqueNonEmptyElementsOf(getProfilesFromFile(config, homeDir+"/.aws/credentials")) 57 | config.Logger.Always("Available profile names are: " + fmt.Sprint("[ "+strings.Join(profilesInCredentials, ", ")+" ]")) 58 | config.Logger.GetInput("Profile", &profile) 59 | for !helpers.SliceContains(profilesInCredentials, profile) { 60 | config.Logger.Always("Invalid profile name. Available profile names are: " + fmt.Sprint("[ "+strings.Join(profilesInCredentials, ", ")+" ]")) 61 | config.Logger.GetInput("Profile", &profile) 62 | } 63 | region := getUserRegion(config) 64 | CreateAWSConfigFile(config, profile, region, "") 65 | } 66 | 67 | func setProfileInfoAndCreateConfigFile(config *configuration.Config) (profile string) { 68 | var ans string 69 | config.Logger.GetInput("Do you want to create a default profile with default values? *Y* / *N*", &ans) 70 | if strings.ToUpper(ans) == "Y" { 71 | region := "us-east-1" 72 | profile = "default" 73 | CreateAWSConfigFile(config, profile, region, "JSON") 74 | } else { 75 | profile = getUserProfile(config) 76 | region := getUserRegion(config) 77 | CreateAWSConfigFile(config, profile, region, "") 78 | } 79 | return profile 80 | } 81 | 82 | func addProfileToCredentials(profile string, homePath string, config *configuration.Config) { 83 | profilesInCredentials := getProfilesFromFile(config, homePath+"/.aws/credentials") 84 | if !helpers.SliceContains(profilesInCredentials, profile) { 85 | CreateAWSCredentialsFile(config, profile) 86 | } 87 | } 88 | 89 | func appendStringToFile(path, text string) error { 90 | f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) 91 | if err != nil { 92 | return err 93 | } 94 | defer f.Close() 95 | 96 | _, err = f.WriteString(text) 97 | if err != nil { 98 | return err 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /configuration/mfa.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "errors" 5 | "github.com/Appliscale/perun/utilities" 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/sts" 9 | "github.com/go-ini/ini" 10 | "os" 11 | "os/user" 12 | "time" 13 | ) 14 | 15 | const dateFormat = "2006-01-02 15:04:05" 16 | 17 | func InitialiseMFA(config Config) { 18 | tokenError := UpdateSessionToken(config, config.Profile, (*config.Regions)[0]) 19 | if tokenError != nil { 20 | config.Logger.Error(tokenError.Error()) 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | func UpdateSessionToken(config Config, profile string, region string) error { 26 | if config.Mfa { 27 | currentUser, userError := user.Current() 28 | if userError != nil { 29 | return userError 30 | } 31 | 32 | credentialsFilePath := currentUser.HomeDir + "/.aws/credentials" 33 | configuration, loadCredentialsError := ini.Load(credentialsFilePath) 34 | if loadCredentialsError != nil { 35 | return loadCredentialsError 36 | } 37 | 38 | section, sectionError := configuration.GetSection(profile) 39 | if sectionError != nil { 40 | section, sectionError = configuration.NewSection(profile) 41 | if sectionError != nil { 42 | return sectionError 43 | } 44 | } 45 | 46 | profileLongTerm := profile + "-long-term" 47 | sectionLongTerm, profileLongTermError := configuration.GetSection(profileLongTerm) 48 | if profileLongTermError != nil { 49 | return profileLongTermError 50 | } 51 | 52 | sessionToken := section.Key("aws_session_token") 53 | expiration := section.Key("expiration") 54 | 55 | expirationDate, dataError := time.Parse(dateFormat, section.Key("expiration").Value()) 56 | if dataError == nil { 57 | config.Logger.Info("Session token will expire in " + utilities.TruncateDuration(time.Since(expirationDate)).String() + " (" + expirationDate.Format(dateFormat) + ")") 58 | } 59 | 60 | mfaDevice := sectionLongTerm.Key("mfa_serial").Value() 61 | if mfaDevice == "" { 62 | return errors.New("There is no mfa_serial for the profile " + profileLongTerm + ". If you haven't used --mfa option you can change the default decision for MFA in the configuration file") 63 | } 64 | 65 | if sessionToken.Value() == "" || expiration.Value() == "" || time.Since(expirationDate).Nanoseconds() > 0 { 66 | currentSession, sessionError := session.NewSessionWithOptions( 67 | session.Options{ 68 | Config: aws.Config{ 69 | Region: ®ion, 70 | }, 71 | Profile: profileLongTerm, 72 | }) 73 | if sessionError != nil { 74 | return sessionError 75 | } 76 | 77 | var duration int64 78 | if config.MfaDuration == 0 { 79 | sessionError = config.Logger.GetInput("Duration", &duration) 80 | if sessionError != nil { 81 | return sessionError 82 | } 83 | } else { 84 | duration = config.MfaDuration 85 | } 86 | 87 | var tokenCode string 88 | sessionError = config.Logger.GetInput("MFA token code", &tokenCode) 89 | if sessionError != nil { 90 | return sessionError 91 | } 92 | 93 | stsSession := sts.New(currentSession) 94 | newToken, tokenError := stsSession.GetSessionToken(&sts.GetSessionTokenInput{ 95 | DurationSeconds: &duration, 96 | SerialNumber: aws.String(mfaDevice), 97 | TokenCode: &tokenCode, 98 | }) 99 | if tokenError != nil { 100 | return tokenError 101 | } 102 | 103 | section.Key("aws_access_key_id").SetValue(*newToken.Credentials.AccessKeyId) 104 | section.Key("aws_secret_access_key").SetValue(*newToken.Credentials.SecretAccessKey) 105 | sessionToken.SetValue(*newToken.Credentials.SessionToken) 106 | section.Key("expiration").SetValue(newToken.Credentials.Expiration.Format(dateFormat)) 107 | 108 | configuration.SaveTo(credentialsFilePath) 109 | } 110 | } 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /resource/s3_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/Appliscale/cloud-security-audit/configuration" 8 | "github.com/Appliscale/cloud-security-audit/csasession" 9 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory/mocks" 10 | "github.com/aws/aws-sdk-go/service/s3" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestS3_ActionUnmarshalJSONCreateSliceOfStringsFromJsonArray(t *testing.T) { 15 | b := []byte(`["s3:GetObject","s3:GetBucketLocation"]`) 16 | actions := Actions{} 17 | err := actions.UnmarshalJSON(b) 18 | assert.Nilf(t, err, "UnmarshalJSON should not return error for array of actions.") 19 | assert.Equalf(t, 2, len(actions), "Actions should contain two elements.") 20 | } 21 | 22 | func TestS3_ActionUnmarshalJSONCreateSliceOfStringsFromJsonString(t *testing.T) { 23 | b := []byte(`"s3:GetObject"`) 24 | actions := Actions{} 25 | err := actions.UnmarshalJSON(b) 26 | assert.Nilf(t, err, "UnmarshalJSON should not return error for string in actions object.") 27 | assert.Equalf(t, 1, len(actions), "Actions should contain two elements.") 28 | } 29 | 30 | func TestS3_ActionUnmarshalJSONReturnsErrorFromJsonMap(t *testing.T) { 31 | b := []byte(`{"something":{"s3":"GetObject"}}`) 32 | actions := Actions{} 33 | err := actions.UnmarshalJSON(b) 34 | assert.NotNilf(t, err, "UnmarshalJSON should return error for Json Map") 35 | } 36 | 37 | func TestS3_PrincipalUnmarshalJSONCreatesMapOfSlicesIfJSONPropertiesAreMapOfArrays(t *testing.T) { 38 | b := []byte(`{"AWS": ["something","something2"]}`) 39 | principal := Principal{} 40 | err := principal.UnmarshalJSON(b) 41 | assert.Nilf(t, err, "This should not return error") 42 | assert.Equal(t, 2, len(principal.Map["AWS"])) 43 | } 44 | 45 | func TestS3_PrincipalUnmarshalJSONCreateMapOfSliceIfJSonPropertyIsMap(t *testing.T) { 46 | b := []byte(`{"Service":"blabla"}`) 47 | principal := Principal{} 48 | err := principal.UnmarshalJSON(b) 49 | assert.Nilf(t, err, "This should not return error") 50 | assert.Equal(t, 1, len(principal.Map["Service"])) 51 | } 52 | 53 | func TestS3_PrincipalUnmarshalJSONAssignWildcardIfJsonPropertyIsString(t *testing.T) { 54 | b := []byte(`"*"`) 55 | principal := Principal{} 56 | err := principal.UnmarshalJSON(b) 57 | assert.Nilf(t, err, "This should not return error") 58 | fmt.Printf("\n%v\n", principal) 59 | assert.Equal(t, "*", principal.Wildcard) 60 | } 61 | 62 | func TestS3Buckets_LoadNames(t *testing.T) { 63 | config := configuration.GetTestConfig(t) 64 | defer config.ClientFactory.(*mocks.ClientFactoryMock).Destroy() 65 | 66 | ec2Client, _ := config.ClientFactory.GetS3Client(csasession.SessionConfig{}) 67 | ec2Client.(*mocks.MockS3Client). 68 | EXPECT(). 69 | ListBuckets(&s3.ListBucketsInput{}). 70 | Times(1). 71 | Return(&s3.ListBucketsOutput{}, nil) 72 | 73 | s3Bucket := &S3Buckets{} 74 | s3Bucket.LoadNames(&config, "region") 75 | 76 | } 77 | 78 | func TestResourcesWithSlice(t *testing.T) { 79 | b := []byte(`["arn:aws:s3:::examplebucket/*"]`) 80 | resources := Resources{} 81 | err := resources.UnmarshalJSON(b) 82 | assert.Nilf(t, err, "UnmarshalJSON should not return error for array of actions.") 83 | } 84 | 85 | func TestResourcesWithTwoElements(t *testing.T) { 86 | b := []byte(`["arn:aws:iam::111122223333:root","arn:aws:iam::444455556666:root"]`) 87 | resources := Resources{} 88 | err := resources.UnmarshalJSON(b) 89 | assert.Nilf(t, err, "UnmarshalJSON should not return error for array of resources.") 90 | assert.Equalf(t, 2, len(resources), "Resources should contain two elements.") 91 | } 92 | 93 | func TestResourcesWithOneElement(t *testing.T) { 94 | b := []byte(`"s3:GetObject"`) 95 | resources := Resources{} 96 | err := resources.UnmarshalJSON(b) 97 | assert.Nilf(t, err, "UnmarshalJSON should not return error for string in actions object.") 98 | assert.Equalf(t, 1, len(resources), "Resources should contain one element.") 99 | } 100 | 101 | func TestResourcesWithMap(t *testing.T) { 102 | b := []byte(`{"something":{"s3":"GetObject"}}`) 103 | resources := Resources{} 104 | err := resources.UnmarshalJSON(b) 105 | assert.NotNilf(t, err, "UnmarshalJSON should return error for Json Map.") 106 | } 107 | -------------------------------------------------------------------------------- /csasession/clientfactory/mocks/ec2client_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./csasession/clientfactory/ec2client.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | ec2 "github.com/aws/aws-sdk-go/service/ec2" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockEC2Client is a mock of EC2Client interface 14 | type MockEC2Client struct { 15 | ctrl *gomock.Controller 16 | recorder *MockEC2ClientMockRecorder 17 | } 18 | 19 | // MockEC2ClientMockRecorder is the mock recorder for MockEC2Client 20 | type MockEC2ClientMockRecorder struct { 21 | mock *MockEC2Client 22 | } 23 | 24 | // NewMockEC2Client creates a new mock instance 25 | func NewMockEC2Client(ctrl *gomock.Controller) *MockEC2Client { 26 | mock := &MockEC2Client{ctrl: ctrl} 27 | mock.recorder = &MockEC2ClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockEC2Client) EXPECT() *MockEC2ClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // DescribeInstances mocks base method 37 | func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "DescribeInstances", input) 40 | ret0, _ := ret[0].(*ec2.DescribeInstancesOutput) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // DescribeInstances indicates an expected call of DescribeInstances 46 | func (mr *MockEC2ClientMockRecorder) DescribeInstances(input interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeInstances", reflect.TypeOf((*MockEC2Client)(nil).DescribeInstances), input) 49 | } 50 | 51 | // DescribeVolumes mocks base method 52 | func (m *MockEC2Client) DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "DescribeVolumes", input) 55 | ret0, _ := ret[0].(*ec2.DescribeVolumesOutput) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // DescribeVolumes indicates an expected call of DescribeVolumes 61 | func (mr *MockEC2ClientMockRecorder) DescribeVolumes(input interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeVolumes", reflect.TypeOf((*MockEC2Client)(nil).DescribeVolumes), input) 64 | } 65 | 66 | // DescribeSecurityGroups mocks base method 67 | func (m *MockEC2Client) DescribeSecurityGroups(input *ec2.DescribeSecurityGroupsInput) (*ec2.DescribeSecurityGroupsOutput, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "DescribeSecurityGroups", input) 70 | ret0, _ := ret[0].(*ec2.DescribeSecurityGroupsOutput) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // DescribeSecurityGroups indicates an expected call of DescribeSecurityGroups 76 | func (mr *MockEC2ClientMockRecorder) DescribeSecurityGroups(input interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecurityGroups", reflect.TypeOf((*MockEC2Client)(nil).DescribeSecurityGroups), input) 79 | } 80 | 81 | // DescribeImages mocks base method 82 | func (m *MockEC2Client) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "DescribeImages", input) 85 | ret0, _ := ret[0].(*ec2.DescribeImagesOutput) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // DescribeImages indicates an expected call of DescribeImages 91 | func (mr *MockEC2ClientMockRecorder) DescribeImages(input interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImages", reflect.TypeOf((*MockEC2Client)(nil).DescribeImages), input) 94 | } 95 | 96 | // DescribeSnapshots mocks base method 97 | func (m *MockEC2Client) DescribeSnapshots(input *ec2.DescribeSnapshotsInput) (*ec2.DescribeSnapshotsOutput, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "DescribeSnapshots", input) 100 | ret0, _ := ret[0].(*ec2.DescribeSnapshotsOutput) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // DescribeSnapshots indicates an expected call of DescribeSnapshots 106 | func (mr *MockEC2ClientMockRecorder) DescribeSnapshots(input interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSnapshots", reflect.TypeOf((*MockEC2Client)(nil).DescribeSnapshots), input) 109 | } 110 | -------------------------------------------------------------------------------- /report/ec2report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "bytes" 5 | "github.com/Appliscale/cloud-security-audit/configuration" 6 | "github.com/Appliscale/cloud-security-audit/environment" 7 | "github.com/Appliscale/cloud-security-audit/resource" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type Ec2Report struct { 14 | VolumeReport *VolumeReport 15 | InstanceID string 16 | SortableTags *SortableTags 17 | SecurityGroupsIDs []string 18 | AvailabilityZone string 19 | } 20 | 21 | func NewEc2Report(instanceID string) *Ec2Report { 22 | return &Ec2Report{ 23 | InstanceID: instanceID, 24 | VolumeReport: &VolumeReport{}, 25 | SortableTags: NewSortableTags(), 26 | } 27 | } 28 | 29 | type Ec2Reports []*Ec2Report 30 | 31 | type Ec2ReportRequiredResources struct { 32 | Ec2s *resource.Ec2s 33 | KMSKeys *resource.KMSKeys 34 | Volumes *resource.Volumes 35 | SecurityGroups *resource.SecurityGroups 36 | AvailabilityZone string 37 | } 38 | 39 | func (e *Ec2Reports) GetHeaders() []string { 40 | return []string{"Availability\nZone", "EC2", "Volumes\n(None) - not encrypted\n(DKMS) - encrypted with default KMSKey", "Security\nGroups\n(Incoming CIDR = 0\x2E0\x2E0\x2E0/0)\nID : PROTOCOL : PORT", "EC2 Tags"} 41 | } 42 | 43 | func (e *Ec2Reports) FormatDataToTable() [][]string { 44 | data := [][]string{} 45 | 46 | for _, ec2Report := range *e { 47 | row := []string{ 48 | ec2Report.AvailabilityZone, 49 | ec2Report.InstanceID, 50 | ec2Report.VolumeReport.ToTableData(), 51 | SliceOfStringsToString(ec2Report.SecurityGroupsIDs), 52 | ec2Report.SortableTags.ToTableData(), 53 | } 54 | data = append(data, row) 55 | } 56 | sortedData := sortTableData(data) 57 | return sortedData 58 | } 59 | 60 | func (e *Ec2Reports) GenerateReport(r *Ec2ReportRequiredResources) { 61 | for _, ec2 := range *r.Ec2s { 62 | ec2Report := NewEc2Report(*ec2.InstanceId) 63 | ec2OK := true 64 | for _, blockDeviceMapping := range ec2.BlockDeviceMappings { 65 | volume := r.Volumes.FindById(*blockDeviceMapping.Ebs.VolumeId) 66 | if !*volume.Encrypted { 67 | ec2OK = false 68 | ec2Report.VolumeReport.AddEBS(*volume.VolumeId, NONE) 69 | } else { 70 | kmskey := r.KMSKeys.FindByKeyArn(*volume.KmsKeyId) 71 | if !kmskey.Custom { 72 | ec2OK = false 73 | ec2Report.VolumeReport.AddEBS(*volume.VolumeId, DKMS) 74 | } 75 | } 76 | } 77 | 78 | for _, sg := range ec2.SecurityGroups { 79 | ipPermissions := r.SecurityGroups.GetIpPermissionsByID(*sg.GroupId) 80 | if ipPermissions != nil { 81 | for _, ipPermission := range ipPermissions { 82 | for _, ipRange := range ipPermission.IpRanges { 83 | if *ipRange.CidrIp == "0.0.0.0/0" { 84 | ec2Report.SecurityGroupsIDs = append(ec2Report.SecurityGroupsIDs, *sg.GroupId+" : "+*ipPermission.IpProtocol+" : "+strconv.FormatInt(*ipPermission.ToPort, 10)) 85 | ec2OK = false 86 | } 87 | } 88 | } 89 | } 90 | } 91 | if !ec2OK { 92 | ec2Report.SortableTags.Add(ec2.Tags) 93 | *e = append(*e, ec2Report) 94 | } 95 | ec2Report.AvailabilityZone = *ec2.Placement.AvailabilityZone 96 | } 97 | } 98 | 99 | // GetResources : Initialize and loads required resources to create ec2 report 100 | func (e *Ec2Reports) GetResources(config *configuration.Config) (*Ec2ReportRequiredResources, error) { 101 | resources := &Ec2ReportRequiredResources{ 102 | KMSKeys: resource.NewKMSKeys(), 103 | Ec2s: &resource.Ec2s{}, 104 | Volumes: &resource.Volumes{}, 105 | SecurityGroups: &resource.SecurityGroups{}, 106 | AvailabilityZone: "zone", 107 | } 108 | 109 | for _, region := range *config.Regions { 110 | err := resource.LoadResources( 111 | config, 112 | region, 113 | resources.Ec2s, 114 | resources.KMSKeys, 115 | resources.Volumes, 116 | resources.SecurityGroups, 117 | ) 118 | if err != nil { 119 | return nil, err 120 | } 121 | } 122 | return resources, nil 123 | } 124 | 125 | func SliceOfStringsToString(slice []string) string { 126 | n := len(slice) 127 | if n == 0 { 128 | return "" 129 | } 130 | var buffer bytes.Buffer 131 | for _, s := range slice[:n-1] { 132 | buffer.WriteString(s + "\n") 133 | } 134 | buffer.WriteString(slice[n-1]) 135 | return buffer.String() 136 | } 137 | 138 | func sortTableData(data [][]string) [][]string { 139 | if data[0][0] == "" { 140 | return data 141 | } 142 | var regions []string 143 | var sortedData [][]string 144 | for _, regs := range data { 145 | reg := regs[0][:len(regs[0])-1] 146 | regions = append(regions, reg) 147 | } 148 | sort.Strings(regions) 149 | uniqueregions := environment.UniqueNonEmptyElementsOf(regions) 150 | for _, unique := range uniqueregions { 151 | for _, b := range data { 152 | if strings.Contains(b[0], unique) { 153 | sortedData = append(sortedData, b) 154 | } 155 | } 156 | } 157 | return sortedData 158 | } 159 | -------------------------------------------------------------------------------- /csasession/clientfactory/mocks/s3client_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./csasession/clientfactory/s3client.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | s3 "github.com/aws/aws-sdk-go/service/s3" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockS3Client is a mock of S3Client interface 14 | type MockS3Client struct { 15 | ctrl *gomock.Controller 16 | recorder *MockS3ClientMockRecorder 17 | } 18 | 19 | // MockS3ClientMockRecorder is the mock recorder for MockS3Client 20 | type MockS3ClientMockRecorder struct { 21 | mock *MockS3Client 22 | } 23 | 24 | // NewMockS3Client creates a new mock instance 25 | func NewMockS3Client(ctrl *gomock.Controller) *MockS3Client { 26 | mock := &MockS3Client{ctrl: ctrl} 27 | mock.recorder = &MockS3ClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockS3Client) EXPECT() *MockS3ClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetBucketPolicy mocks base method 37 | func (m *MockS3Client) GetBucketPolicy(input *s3.GetBucketPolicyInput) (*s3.GetBucketPolicyOutput, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "GetBucketPolicy", input) 40 | ret0, _ := ret[0].(*s3.GetBucketPolicyOutput) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // GetBucketPolicy indicates an expected call of GetBucketPolicy 46 | func (mr *MockS3ClientMockRecorder) GetBucketPolicy(input interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicy", reflect.TypeOf((*MockS3Client)(nil).GetBucketPolicy), input) 49 | } 50 | 51 | // GetBucketEncryption mocks base method 52 | func (m *MockS3Client) GetBucketEncryption(input *s3.GetBucketEncryptionInput) (*s3.GetBucketEncryptionOutput, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "GetBucketEncryption", input) 55 | ret0, _ := ret[0].(*s3.GetBucketEncryptionOutput) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // GetBucketEncryption indicates an expected call of GetBucketEncryption 61 | func (mr *MockS3ClientMockRecorder) GetBucketEncryption(input interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketEncryption", reflect.TypeOf((*MockS3Client)(nil).GetBucketEncryption), input) 64 | } 65 | 66 | // GetBucketLogging mocks base method 67 | func (m *MockS3Client) GetBucketLogging(input *s3.GetBucketLoggingInput) (*s3.GetBucketLoggingOutput, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "GetBucketLogging", input) 70 | ret0, _ := ret[0].(*s3.GetBucketLoggingOutput) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // GetBucketLogging indicates an expected call of GetBucketLogging 76 | func (mr *MockS3ClientMockRecorder) GetBucketLogging(input interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLogging", reflect.TypeOf((*MockS3Client)(nil).GetBucketLogging), input) 79 | } 80 | 81 | // GetBucketAcl mocks base method 82 | func (m *MockS3Client) GetBucketAcl(input *s3.GetBucketAclInput) (*s3.GetBucketAclOutput, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "GetBucketAcl", input) 85 | ret0, _ := ret[0].(*s3.GetBucketAclOutput) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // GetBucketAcl indicates an expected call of GetBucketAcl 91 | func (mr *MockS3ClientMockRecorder) GetBucketAcl(input interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAcl", reflect.TypeOf((*MockS3Client)(nil).GetBucketAcl), input) 94 | } 95 | 96 | // ListBuckets mocks base method 97 | func (m *MockS3Client) ListBuckets(input *s3.ListBucketsInput) (*s3.ListBucketsOutput, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "ListBuckets", input) 100 | ret0, _ := ret[0].(*s3.ListBucketsOutput) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // ListBuckets indicates an expected call of ListBuckets 106 | func (mr *MockS3ClientMockRecorder) ListBuckets(input interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockS3Client)(nil).ListBuckets), input) 109 | } 110 | 111 | // GetBucketLocation mocks base method 112 | func (m *MockS3Client) GetBucketLocation(input *s3.GetBucketLocationInput) (*s3.GetBucketLocationOutput, error) { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "GetBucketLocation", input) 115 | ret0, _ := ret[0].(*s3.GetBucketLocationOutput) 116 | ret1, _ := ret[1].(error) 117 | return ret0, ret1 118 | } 119 | 120 | // GetBucketLocation indicates an expected call of GetBucketLocation 121 | func (mr *MockS3ClientMockRecorder) GetBucketLocation(input interface{}) *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLocation", reflect.TypeOf((*MockS3Client)(nil).GetBucketLocation), input) 124 | } 125 | -------------------------------------------------------------------------------- /resource/kmskeys.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/Appliscale/cloud-security-audit/configuration" 9 | "github.com/Appliscale/cloud-security-audit/csasession" 10 | 11 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory" 12 | "github.com/aws/aws-sdk-go/aws/awserr" 13 | "github.com/aws/aws-sdk-go/service/kms" 14 | ) 15 | 16 | type KMSKey struct { 17 | AliasArn string 18 | AliasName string 19 | Custom bool 20 | KeyId string // the same as TargetKeyId in AliasListEntry 21 | } 22 | 23 | type KMSKeys struct { 24 | Values map[string]*KMSKey 25 | sync.RWMutex 26 | } 27 | 28 | // NewKMSKeys : Initialize KMS Keys struct with map of keys 29 | func NewKMSKeys() *KMSKeys { 30 | return &KMSKeys{Values: make(map[string]*KMSKey)} 31 | } 32 | 33 | type KMSKeyAliases []*kms.AliasListEntry 34 | 35 | type KMSKeysListEntries []*kms.KeyListEntry 36 | 37 | // LoadAllFromAWS : Load KMS Keys from all regions 38 | func (k *KMSKeys) LoadAllFromAWS(config *configuration.Config) error { 39 | regions := *csasession.GetAvailableRegions() 40 | 41 | var wg sync.WaitGroup 42 | n := len(regions) * 2 43 | done := make(chan bool, n) 44 | errc := make(chan error, n) 45 | wg.Add(n) 46 | 47 | go func() { 48 | wg.Wait() 49 | close(done) 50 | close(errc) 51 | }() 52 | 53 | kmsKeyAliases := &KMSKeyAliases{} 54 | kmsKeyListEntries := &KMSKeysListEntries{} 55 | for _, region := range regions { 56 | kmsClient, err := config.ClientFactory.GetKmsClient(csasession.SessionConfig{Profile: config.Profile, Region: region}) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | go loadKeyListEntries(kmsClient, kmsKeyListEntries, done, errc, &wg) 62 | go loadKeyAliases(kmsClient, kmsKeyAliases, done, errc, &wg) 63 | } 64 | for i := 0; i < n; i++ { 65 | select { 66 | case <-done: 67 | case err := <-errc: 68 | return err 69 | } 70 | } 71 | 72 | k.loadValuesToMap(kmsKeyAliases, kmsKeyListEntries) 73 | 74 | return nil 75 | } 76 | 77 | func loadKeyListEntries(kmsAPI clientfactory.KmsClient, keyListEntries *KMSKeysListEntries, done chan bool, errc chan error, wg *sync.WaitGroup) { 78 | defer wg.Done() 79 | q := &kms.ListKeysInput{} 80 | for { 81 | result, err := kmsAPI.ListKeys(q) 82 | if err != nil { 83 | if aerr, ok := err.(awserr.Error); ok { 84 | switch aerr.Code() { 85 | case "SubscriptionRequiredException": 86 | done <- true 87 | default: 88 | errc <- fmt.Errorf("[AWS-ERROR] Error Msg: %s", aerr.Error()) 89 | } 90 | } else { 91 | errc <- fmt.Errorf("[ERROR] Error Msg: %s", err.Error()) 92 | } 93 | return 94 | } 95 | if len(result.Keys) == 0 { 96 | done <- true 97 | return 98 | } 99 | 100 | *keyListEntries = append(*keyListEntries, result.Keys...) 101 | if !*result.Truncated { 102 | done <- true 103 | break 104 | } 105 | q.Marker = result.NextMarker 106 | } 107 | } 108 | 109 | func loadKeyAliases(kmsAPI clientfactory.KmsClient, aliases *KMSKeyAliases, done chan bool, errc chan error, wg *sync.WaitGroup) { 110 | defer wg.Done() 111 | listAliasesInput := &kms.ListAliasesInput{} 112 | for { 113 | result, err := kmsAPI.ListAliases(listAliasesInput) 114 | if err != nil { 115 | if aerr, ok := err.(awserr.Error); ok { 116 | switch aerr.Code() { 117 | case "SubscriptionRequiredException": 118 | done <- true 119 | default: 120 | errc <- fmt.Errorf("[AWS-ERROR] Error Msg: %s", aerr.Error()) 121 | } 122 | } else { 123 | errc <- fmt.Errorf("[ERROR] Error Msg: %s", err.Error()) 124 | } 125 | return 126 | } 127 | *aliases = append(*aliases, result.Aliases...) 128 | 129 | if !*result.Truncated { 130 | done <- true 131 | break 132 | } 133 | listAliasesInput.Marker = result.NextMarker 134 | } 135 | } 136 | 137 | func (k *KMSKeys) LoadFromAWS(config *configuration.Config, region string) error { 138 | kmsAPI, err := config.ClientFactory.GetKmsClient(csasession.SessionConfig{Profile: config.Profile, Region: region}) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | var wg sync.WaitGroup 144 | n := 2 145 | done := make(chan bool, n) 146 | errc := make(chan error, n) 147 | wg.Add(n) 148 | 149 | go func() { 150 | wg.Wait() 151 | close(done) 152 | close(errc) 153 | }() 154 | 155 | kmsKeyAliases := &KMSKeyAliases{} 156 | kmsKeyListEntries := &KMSKeysListEntries{} 157 | 158 | go loadKeyListEntries(kmsAPI, kmsKeyListEntries, done, errc, &wg) 159 | go loadKeyAliases(kmsAPI, kmsKeyAliases, done, errc, &wg) 160 | 161 | for i := 0; i < n; i++ { 162 | select { 163 | case <-done: 164 | case err := <-errc: 165 | return err 166 | } 167 | } 168 | 169 | k.loadValuesToMap(kmsKeyAliases, kmsKeyListEntries) 170 | 171 | return nil 172 | } 173 | 174 | func (k *KMSKeys) loadValuesToMap(aliases *KMSKeyAliases, keyListEntries *KMSKeysListEntries) { 175 | for _, keyListEntry := range *keyListEntries { 176 | key := KMSKey{KeyId: *keyListEntry.KeyId} 177 | for _, alias := range *aliases { 178 | if alias.TargetKeyId != nil { 179 | if key.KeyId == *alias.TargetKeyId { 180 | key.AliasArn = *alias.AliasArn 181 | key.AliasName = *alias.AliasName 182 | if !strings.Contains(*alias.AliasName, "alias/aws/") { 183 | key.Custom = true 184 | } 185 | break 186 | } 187 | } else { 188 | key.Custom = true 189 | } 190 | } 191 | k.Values[*keyListEntry.KeyArn] = &key 192 | } 193 | } 194 | 195 | func (k *KMSKeys) FindByKeyArn(keyArn string) *KMSKey { 196 | kmsKey, ok := k.Values[keyArn] 197 | if ok { 198 | return kmsKey 199 | } 200 | return nil 201 | } 202 | -------------------------------------------------------------------------------- /environment/checkfiles.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "bufio" 5 | "github.com/Appliscale/cloud-security-audit/configuration" 6 | "github.com/Appliscale/perun/helpers" 7 | "github.com/aws/aws-sdk-go/aws/endpoints" 8 | "os" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var Regions = getAllRegions() 14 | 15 | func CheckAWSConfigFiles(config *configuration.Config) bool { 16 | homeDir, pathError := GetUserHomeDir() 17 | if pathError != nil { 18 | config.Logger.Error(pathError.Error()) 19 | return false 20 | } 21 | configAWSExists, configError := isAWSConfigPresent(homeDir) 22 | if configError != nil { 23 | config.Logger.Error(configError.Error()) 24 | } 25 | 26 | credentialsExists, credentialsError := isCredentialsPresent(homeDir) 27 | if credentialsError != nil { 28 | config.Logger.Error(credentialsError.Error()) 29 | } 30 | 31 | profile := config.Profile 32 | if configAWSExists { 33 | profilesInConfig := getProfilesFromFile(config, homeDir+"/.aws/config") 34 | if !helpers.SliceContains(profilesInConfig, profile) { 35 | var ans string 36 | config.Logger.GetInput("You don't have the "+profile+" profile in your config file. Would you like to create one? *Y* / *N*", &ans) 37 | if strings.ToUpper(ans) == "Y" { 38 | region := getUserRegion(config) 39 | CreateAWSConfigFile(config, profile, region, "") 40 | } else { 41 | config.Logger.Info("You can use another profile by setting the \"-p\" argument or specify a different default profile by setting the AWS_PROFILE variable") 42 | return false 43 | } 44 | } 45 | if credentialsExists { 46 | addProfileToCredentials(profile, homeDir, config) 47 | } else { 48 | CreateAWSCredentialsFile(config, profile) 49 | } 50 | } else { 51 | if credentialsExists { 52 | var ans string 53 | config.Logger.GetInput("File .aws/config does not exist, but .aws/credentials has been found. Do you want to create config file using one of the profiles in the .aws/credentias? *Y* / *N*", &ans) 54 | if strings.ToUpper(ans) == "Y" { 55 | createConfigProfileFromCredentials(homeDir, config, profile) 56 | return true 57 | 58 | } else { 59 | profile = setProfileInfoAndCreateConfigFile(config) 60 | CreateAWSCredentialsFile(config, profile) 61 | } 62 | } else { 63 | config.Logger.Info("File .aws/config does not exist.") 64 | profile = setProfileInfoAndCreateConfigFile(config) 65 | CreateAWSCredentialsFile(config, profile) 66 | } 67 | } 68 | return true 69 | } 70 | 71 | func isAWSConfigPresent(homePath string) (bool, error) { 72 | _, credentialsError := os.Open(homePath + "/.aws/config") 73 | if credentialsError != nil { 74 | return false, nil 75 | } 76 | return true, nil 77 | } 78 | 79 | func isCredentialsPresent(homePath string) (bool, error) { 80 | _, credentialsError := os.Open(homePath + "/.aws/credentials") 81 | if credentialsError != nil { 82 | return false, nil 83 | } 84 | return true, nil 85 | } 86 | 87 | func getAllRegions() (Regions []string) { 88 | rs, _ := endpoints.RegionsForService(endpoints.DefaultPartitions(), endpoints.AwsPartitionID, endpoints.ApigatewayServiceID) 89 | for region := range rs { 90 | Regions = append(Regions, region) 91 | } 92 | return 93 | } 94 | 95 | func getUserRegion(config *configuration.Config) string { 96 | showAvailableRegions(config) 97 | var numberRegion int 98 | config.Logger.GetInput("Region", &numberRegion) 99 | 100 | for numberRegion < 0 || numberRegion >= len(Regions) { 101 | config.Logger.Always("Try again, invalid region") 102 | config.Logger.GetInput("Region", &numberRegion) 103 | } 104 | region := Regions[numberRegion] 105 | config.Logger.Always("Your region is: " + region) 106 | return region 107 | } 108 | 109 | func showAvailableRegions(config *configuration.Config) { 110 | config.Logger.Always("Available Regions:") 111 | for i := 0; i < len(Regions); i++ { 112 | pom := strconv.Itoa(i) 113 | config.Logger.Always("Number " + pom + " region " + Regions[i]) 114 | } 115 | } 116 | 117 | func getUserOutput(config *configuration.Config) string { 118 | var output string 119 | config.Logger.GetInput("Input the output format [json, text, table]", &output) 120 | for !helpers.SliceContains([]string{"json", "text", "table"}, output) { 121 | config.Logger.Always("Try again, invalid output") 122 | config.Logger.GetInput("Input the output format [json, text, table]", &output) 123 | } 124 | config.Logger.Always("Your output is: " + output) 125 | return output 126 | } 127 | 128 | func getUserProfile(config *configuration.Config) string { 129 | var profile string 130 | config.Logger.GetInput("Input name of profile", &profile) 131 | for profile == "" { 132 | config.Logger.Always("Try again, invalid profile") 133 | config.Logger.GetInput("Input name of profile", &profile) 134 | } 135 | config.Logger.Always("Your region is: " + profile) 136 | return profile 137 | } 138 | 139 | func getProfilesFromFile(config *configuration.Config, path string) []string { 140 | credentials, credentialsError := os.Open(path) 141 | if credentialsError != nil { 142 | config.Logger.Error(credentialsError.Error()) 143 | return []string{} 144 | } 145 | defer credentials.Close() 146 | profiles := make([]string, 0) 147 | scanner := bufio.NewScanner(credentials) 148 | for scanner.Scan() { 149 | if strings.Contains(scanner.Text(), "[") { 150 | profile := strings.TrimPrefix(scanner.Text(), "[") 151 | profile = strings.TrimSuffix(profile, "]") 152 | if strings.Contains(profile, "profile ") { 153 | profile = strings.TrimPrefix(profile, "profile ") 154 | } 155 | if strings.Contains(profile, "-long-term") { 156 | profile = strings.TrimSuffix(profile, "-long-term") 157 | } 158 | profiles = append(profiles, profile) 159 | } 160 | } 161 | return profiles 162 | } 163 | -------------------------------------------------------------------------------- /resource/s3.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/Appliscale/cloud-security-audit/configuration" 8 | "github.com/Appliscale/cloud-security-audit/csasession" 9 | 10 | "github.com/Appliscale/cloud-security-audit/csasession/clientfactory" 11 | "github.com/aws/aws-sdk-go/aws/awserr" 12 | "github.com/aws/aws-sdk-go/service/s3" 13 | ) 14 | 15 | type S3Bucket struct { 16 | *s3.Bucket 17 | S3Policy *S3Policy 18 | Region *string 19 | *s3.ServerSideEncryptionConfiguration 20 | *s3.LoggingEnabled 21 | ACL s3.GetBucketAclOutput 22 | } 23 | 24 | type S3Buckets []*S3Bucket 25 | 26 | func (b *S3Buckets) LoadRegions(config *configuration.Config, region string) error { 27 | sessionConfig := csasession.SessionConfig{Profile: config.Profile, Region: region} 28 | err := config.SessionFactory.SetNormalizeBucketLocation(sessionConfig) 29 | if err != nil { 30 | return err 31 | } 32 | defer config.SessionFactory.ReinitialiseSession(sessionConfig) 33 | 34 | s3API, err := config.ClientFactory.GetS3Client(sessionConfig) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | wg := sync.WaitGroup{} 40 | n := len(*b) 41 | wg.Add(n) 42 | done := make(chan bool, n) 43 | cerrs := make(chan error, n) 44 | 45 | go func() { 46 | wg.Wait() 47 | close(done) 48 | close(cerrs) 49 | }() 50 | 51 | for _, bucket := range *b { 52 | go func(s3Bucket *S3Bucket) { 53 | result, err := s3API.GetBucketLocation(&s3.GetBucketLocationInput{Bucket: s3Bucket.Name}) 54 | if err != nil { 55 | cerrs <- err 56 | return 57 | } 58 | s3Bucket.Region = result.LocationConstraint 59 | done <- true 60 | }(bucket) 61 | } 62 | for i := 0; i < n; i++ { 63 | select { 64 | case <-done: 65 | case err := <-cerrs: 66 | return err 67 | } 68 | } 69 | return nil 70 | 71 | } 72 | 73 | // LoadNames : Get All S3 Bucket names 74 | func (b *S3Buckets) LoadNames(config *configuration.Config, region string) error { 75 | s3API, err := config.ClientFactory.GetS3Client(csasession.SessionConfig{Profile: config.Profile, Region: region}) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | result, err := s3API.ListBuckets(&s3.ListBucketsInput{}) 81 | if err != nil { 82 | return err 83 | } 84 | for _, bucket := range result.Buckets { 85 | *b = append(*b, &S3Bucket{Bucket: bucket}) 86 | } 87 | return nil 88 | } 89 | 90 | func (b *S3Buckets) LoadFromAWS(config *configuration.Config, region string) error { 91 | err := b.LoadNames(config, region) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | err = b.LoadRegions(config, region) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | var wg sync.WaitGroup 102 | // For every S3Bucket b are running 4 functions https://golang.org/pkg/sync/#WaitGroup 103 | n := 4 * len(*b) 104 | done := make(chan bool, n) 105 | errs := make(chan error, n) 106 | wg.Add(n) 107 | 108 | go func() { 109 | wg.Wait() 110 | close(done) 111 | close(errs) 112 | }() 113 | 114 | for _, s3Bucket := range *b { 115 | s3Client, err := config.ClientFactory.GetS3Client( 116 | csasession.SessionConfig{ 117 | Profile: config.Profile, 118 | Region: *s3Bucket.Region, 119 | }) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | go getPolicy(s3Bucket, s3Client, done, errs, &wg) 125 | go getEncryption(s3Bucket, s3Client, done, errs, &wg) 126 | go getBucketLogging(s3Bucket, s3Client, done, errs, &wg) 127 | go getACL(s3Bucket, s3Client, done, errs, &wg) 128 | } 129 | for i := 0; i < n; i++ { 130 | select { 131 | case <-done: 132 | case err := <-errs: 133 | return err 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | func getPolicy(s3Bucket *S3Bucket, s3API clientfactory.S3Client, done chan bool, errc chan error, wg *sync.WaitGroup) { 140 | defer wg.Done() 141 | 142 | result, err := s3API.GetBucketPolicy(&s3.GetBucketPolicyInput{ 143 | Bucket: s3Bucket.Name, 144 | }) 145 | if err != nil { 146 | if aerr, ok := err.(awserr.Error); ok { 147 | switch aerr.Code() { 148 | case "NoSuchBucketPolicy": 149 | done <- true 150 | default: 151 | errc <- fmt.Errorf("[AWS-ERROR] Bucket: %s Error Msg: %s", *s3Bucket.Name, aerr.Error()) 152 | } 153 | } else { 154 | errc <- fmt.Errorf("[ERROR] %s: %s", *s3Bucket.Name, err.Error()) 155 | } 156 | return 157 | } 158 | if result.Policy != nil { 159 | s3Bucket.S3Policy, err = NewS3Policy(*result.Policy) 160 | if err != nil { 161 | errc <- fmt.Errorf("[ERROR] Bucket: %s Error Msg: %s", *s3Bucket.Name, err.Error()) 162 | return 163 | } 164 | } 165 | done <- true 166 | } 167 | 168 | func getACL(s3Bucket *S3Bucket, s3API clientfactory.S3Client, done chan bool, errs chan error, wg *sync.WaitGroup) { 169 | defer wg.Done() 170 | 171 | result, err := s3API.GetBucketAcl(&s3.GetBucketAclInput{ 172 | Bucket: s3Bucket.Name, 173 | }) 174 | if err != nil { 175 | if aerr, ok := err.(awserr.Error); ok { 176 | switch aerr.Code() { 177 | case "NoSuchBucketACL": 178 | done <- true 179 | default: 180 | errs <- fmt.Errorf("[AWS-ERROR] Bucket: %s Error Msg: %s", *s3Bucket.Name, aerr.Error()) 181 | } 182 | } else { 183 | errs <- fmt.Errorf("[ERROR] %s: %s", *s3Bucket.Name, err.Error()) 184 | } 185 | return 186 | } 187 | if result != nil { 188 | s3Bucket.ACL = *result 189 | } 190 | done <- true 191 | } 192 | 193 | func getEncryption(s3Bucket *S3Bucket, s3API clientfactory.S3Client, done chan bool, errs chan error, wg *sync.WaitGroup) { 194 | defer wg.Done() 195 | result, err := s3API.GetBucketEncryption(&s3.GetBucketEncryptionInput{Bucket: s3Bucket.Name}) 196 | 197 | if err != nil { 198 | if aerr, ok := err.(awserr.Error); ok { 199 | switch aerr.Code() { 200 | case "ServerSideEncryptionConfigurationNotFoundError": 201 | done <- true 202 | default: 203 | errs <- fmt.Errorf("[AWS-ERROR] \nBucket: %s \n Error Msg: %s", *s3Bucket.Name, aerr.Error()) 204 | } 205 | } else { 206 | errs <- fmt.Errorf("[ERROR] %s: %s", *s3Bucket.Name, err.Error()) 207 | } 208 | return 209 | } 210 | 211 | if result.ServerSideEncryptionConfiguration != nil { 212 | s3Bucket.ServerSideEncryptionConfiguration = result.ServerSideEncryptionConfiguration 213 | } 214 | done <- true 215 | } 216 | 217 | func getBucketLogging(s3Bucket *S3Bucket, s3API clientfactory.S3Client, done chan bool, errs chan error, wg *sync.WaitGroup) { 218 | defer wg.Done() 219 | result, err := s3API.GetBucketLogging(&s3.GetBucketLoggingInput{Bucket: s3Bucket.Name}) 220 | if err != nil { 221 | errs <- fmt.Errorf("[ERROR] %s: %s", *s3Bucket.Name, err.Error()) 222 | return 223 | } 224 | s3Bucket.LoggingEnabled = result.LoggingEnabled 225 | done <- true 226 | } 227 | -------------------------------------------------------------------------------- /report/s3report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/Appliscale/cloud-security-audit/configuration" 8 | "github.com/Appliscale/cloud-security-audit/resource" 9 | "github.com/aws/aws-sdk-go/service/s3" 10 | ) 11 | 12 | const action = "Action" 13 | const effect = "Effect" 14 | const principal = "Principal" 15 | 16 | type S3BucketReport struct { 17 | Name string 18 | EncryptionType 19 | LoggingEnabled bool 20 | ACLIsPublic string 21 | PolicyIsPublic string 22 | } 23 | 24 | type S3BucketReports []*S3BucketReport 25 | 26 | type S3ReportRequiredResources struct { 27 | KMSKeys *resource.KMSKeys 28 | S3Buckets *resource.S3Buckets 29 | } 30 | 31 | // CheckEncryptionType : Returns Encryption Type (AES256, CKMS, DKMS, NONE) 32 | func (s3br *S3BucketReport) CheckEncryptionType(s3EncryptionType s3.ServerSideEncryptionByDefault, kmsKeys *resource.KMSKeys) { 33 | 34 | switch *s3EncryptionType.SSEAlgorithm { 35 | case "AES256": 36 | s3br.EncryptionType = AES256 37 | case "aws:kms": 38 | kmsKey := kmsKeys.FindByKeyArn(*s3EncryptionType.KMSMasterKeyID) 39 | if kmsKey.Custom { 40 | s3br.EncryptionType = CKMS 41 | } else { 42 | s3br.EncryptionType = DKMS 43 | } 44 | default: 45 | s3br.EncryptionType = NONE 46 | } 47 | } 48 | 49 | func (s3brs *S3BucketReports) GetHeaders() []string { 50 | return []string{"Bucket Name", "Default\nSSE", "Logging\nEnabled", "ACL\nis public\nR - Read\nW - Write\nD - Delete", "Policy\nis public\nR - Read\nW - Write\nD - Delete"} 51 | } 52 | 53 | func (s3brs *S3BucketReports) FormatDataToTable() [][]string { 54 | data := [][]string{} 55 | 56 | for _, s3br := range *s3brs { 57 | row := []string{ 58 | s3br.Name, 59 | s3br.EncryptionType.String(), 60 | strconv.FormatBool(s3br.LoggingEnabled), 61 | s3br.ACLIsPublic, 62 | s3br.PolicyIsPublic, 63 | } 64 | data = append(data, row) 65 | } 66 | return data 67 | } 68 | 69 | func isBucketACLPublic(s3Bucket *resource.S3Bucket) (bool, string) { 70 | 71 | grants := s3Bucket.ACL.Grants 72 | ownerID := s3Bucket.ACL.Owner.ID 73 | accessTypeACL := "" 74 | isPublic := false 75 | 76 | var uriGroups = []string{ 77 | "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", 78 | "http://acs.amazonaws.com/groups/global/AllUsers", 79 | } 80 | var permissionsACL = []string{ 81 | "READ", 82 | "WRITE", 83 | "READ_ACP", 84 | "WRITE_ACP", 85 | "FULL_CONTROL", 86 | } 87 | 88 | for _, grant := range grants { 89 | isPublic = false 90 | granteeURI := grant.Grantee.URI 91 | granteePermission := grant.Permission 92 | granteeID := grant.Grantee.ID 93 | if granteeID != ownerID { 94 | if granteeID != ownerID { 95 | if (granteeURI != nil && isStringInArray(*granteeURI, uriGroups)) && 96 | (granteePermission != nil && isStringInArray(*granteePermission, permissionsACL)) { 97 | if !strings.Contains(accessTypeACL, getTypeOfAccessACL(*granteePermission)) { 98 | accessTypeACL = accessTypeACL + getTypeOfAccessACL(*granteePermission) 99 | } 100 | isPublic = true 101 | } 102 | } 103 | } 104 | } 105 | return isPublic, accessTypeACL 106 | 107 | } 108 | 109 | func getTypeOfAccessACL(granteePermission string) string { 110 | if granteePermission == "FULL_CONTROL" { 111 | return "RWD" 112 | } 113 | accessTypeACL := string(granteePermission[0]) 114 | return accessTypeACL 115 | } 116 | 117 | func isStringInArray(element string, array []string) bool { 118 | for _, arrayElement := range array { 119 | if arrayElement == element { 120 | return true 121 | } 122 | } 123 | return false 124 | } 125 | 126 | func isBucketPolicyPublic(s3Bucket *resource.S3Bucket) (bool, string) { 127 | isPublic := make(map[string]bool) 128 | accessTypePolicy := "" 129 | if s3Bucket.S3Policy != nil { 130 | bucketPolicy := s3Bucket.S3Policy 131 | stat := bucketPolicy.Statements 132 | 133 | for _, element := range stat { 134 | isPublic[effect] = false 135 | isPublic[action] = false 136 | isPublic[principal] = false 137 | accessTypePolicy = "" 138 | 139 | //Effect 140 | if element.Effect == "Allow" { 141 | isPublic[effect] = true 142 | } 143 | //Action 144 | if len(element.Actions) > 0 { 145 | isPublic[action] = true 146 | accessTypePolicy = getTypeOfAccessPolicy(element.Actions) 147 | } 148 | //Principal 149 | if element.Principal.Wildcard != "" && element.Principal.Wildcard == "*" { 150 | isPublic[principal] = true 151 | } else if len(element.Principal.Map) > 0 { 152 | for _, array := range element.Principal.Map { 153 | for _, principal := range array { 154 | if principal == "*" { 155 | isPublic[principal] = true 156 | } 157 | } 158 | } 159 | } 160 | } 161 | if isPublic[action] && isPublic[effect] && isPublic[principal] { 162 | return true, accessTypePolicy 163 | } 164 | } 165 | return false, "" 166 | } 167 | 168 | func getTypeOfAccessPolicy(actions resource.Actions) string { 169 | var types string 170 | for _, action := range actions { 171 | if strings.Contains(action, "Get") || strings.Contains(action, "List") { 172 | types = types + "R" 173 | } else if strings.Contains(action, "Delete") { 174 | types = types + "D" 175 | } else if strings.Contains(action, "Put") || strings.Contains(action, "Create") { 176 | types = types + "W" 177 | } 178 | } 179 | types = "[" + types + "]" 180 | return types 181 | } 182 | 183 | func (s3brs *S3BucketReports) GenerateReport(r *S3ReportRequiredResources) { 184 | 185 | for _, s3Bucket := range *r.S3Buckets { 186 | s3BucketReport := &S3BucketReport{Name: *s3Bucket.Name} 187 | ok := true 188 | if v := s3Bucket.ServerSideEncryptionConfiguration; v != nil { 189 | s3BucketReport.CheckEncryptionType(*v.Rules[0].ApplyServerSideEncryptionByDefault, r.KMSKeys) 190 | } else { 191 | s3BucketReport.EncryptionType = NONE 192 | ok = false 193 | } 194 | if s3Bucket.LoggingEnabled != nil { 195 | s3BucketReport.LoggingEnabled = true 196 | } else { 197 | ok = false 198 | } 199 | policy, policyTypes := isBucketPolicyPublic(s3Bucket) 200 | s3BucketReport.PolicyIsPublic = strconv.FormatBool(policy) + " " + policyTypes 201 | acl, aclTypes := isBucketACLPublic(s3Bucket) 202 | if len(aclTypes) == 0 { 203 | s3BucketReport.ACLIsPublic = strconv.FormatBool(acl) 204 | } else { 205 | s3BucketReport.ACLIsPublic = strconv.FormatBool(acl) + " " + "[" + aclTypes + "]" 206 | } 207 | if !ok { 208 | *s3brs = append(*s3brs, s3BucketReport) 209 | } 210 | } 211 | } 212 | 213 | func (s3brs *S3BucketReports) GetResources(config *configuration.Config) (*S3ReportRequiredResources, error) { 214 | resources := &S3ReportRequiredResources{ 215 | KMSKeys: resource.NewKMSKeys(), 216 | S3Buckets: &resource.S3Buckets{}, 217 | } 218 | 219 | err := resources.S3Buckets.LoadFromAWS(config, (*config.Regions)[0]) 220 | if err != nil { 221 | return nil, err 222 | } 223 | err = resources.KMSKeys.LoadAllFromAWS(config) 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | return resources, nil 229 | } 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Security Audit [![CircleCI](https://circleci.com/gh/Appliscale/cloud-security-audit.svg?style=svg)](https://circleci.com/gh/Appliscale/cloud-security-audit) [![Release](https://img.shields.io/github/release/Appliscale/cloud-security-audit.svg?style=flat-square)](https://github.com/Appliscale/cloud-security-audit/releases/latest) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg)](https://github.com/Appliscale/cloud-security-audit/blob/master/LICENSE.md) [![Go_Report_Card](https://goreportcard.com/badge/github.com/Appliscale/cloud-security-audit?style=flat-square&fuckgithubcache=1)](https://goreportcard.com/report/github.com/Appliscale/cloud-security-audit) [![GoDoc](https://godoc.org/github.com/Appliscale/cloud-security-audit?status.svg)](https://godoc.org/github.com/Appliscale/cloud-security-audit) 2 | 3 | A command line security audit tool for Amazon Web Services 4 | 5 | ## About 6 | Cloud Security Audit is a command line tool that scans for vulnerabilities in your AWS Account. In easy way you will be able to 7 | identify unsecure parts of your infrastructure and prepare your AWS account for security audit. 8 | 9 | ## Installation 10 | Currently Cloud Security Audit does not support any package managers, but the work is in progress. 11 | ### Building from sources 12 | First of all you need to download Cloud Security Audit to your GO workspace: 13 | 14 | ```bash 15 | $GOPATH $ go get github.com/Appliscale/cloud-security-audit 16 | $GOPATH $ cd cloud-security-audit 17 | ``` 18 | 19 | Then build and install configuration for the application inside cloud-security-audit directory by executing: 20 | 21 | ```bash 22 | cloud-security-audit $ make all 23 | ``` 24 | 25 | ## Usage 26 | ### Initialising Session 27 | If you're using MFA you need to tell Cloud Security Audit to authenticate you before trying to connect by using flag `--mfa`. 28 | Example: 29 | ``` 30 | $ cloud-security-audit --service s3 --mfa --mfa-duration 3600 31 | ``` 32 | 33 | ### EC2 Scan 34 | #### How to use 35 | To perform audit on all EC2 instances, type: 36 | ``` 37 | $ cloud-security-audit --service ec2 38 | ``` 39 | You can narrow the audit to a region, by using the flag `-r` or `--region`. Cloud Security Audit also supports AWS profiles - 40 | to specify profile use the flag `-p` or `--profile`. 41 | 42 | #### Example output 43 | 44 | ```bash 45 | 46 | +---------------+---------------------+--------------------------------+-----------------------------------+----------+ 47 | | AVAILABILITY | EC2 | VOLUMES | SECURITY | | 48 | | | | | | EC2 TAGS | 49 | | ZONE | | (NONE) - NOT ENCRYPTED | GROUPS | | 50 | | | | | | | 51 | | | | (DKMS) - ENCRYPTED WITH | (INCOMING CIDR = 0.0.0.0/0) | | 52 | | | | DEFAULT KMSKEY | | | 53 | | | | | ID : PROTOCOL : PORT | | 54 | +---------------+---------------------+--------------------------------+-----------------------------------+----------+ 55 | | eu-central-1a | i-0fa345j6756nb3v23 | vol-0a81288qjd188424d[DKMS] | sg-aaaaaaaa : tcp : 22 | App:some | 56 | | | | vol-0c2834re8dfsd8sdf[NONE] | sg-aaaaaaaa : tcp : 22 | Key:Val | 57 | +---------------+---------------------+--------------------------------+-----------------------------------+----------+ 58 | ``` 59 | 60 | #### How to read it 61 | 62 | 1. First column `AVAILABILITY ZONE` contains information where the instance is placed 63 | 2. Second column `EC2` contains instance ID. 64 | 3. Third column `Volumes` contains IDs of attached volumes(virtual disks) to given EC2. Suffixes meaning: 65 | * `[NONE]` - Volume not encrypted. 66 | * `[DKMS]` - Volume encrypted using AWS Default KMS Key. More about KMS you can find [here](https://aws.amazon.com/kms/faqs/) 67 | 4. Fourth column `Security Groups` contains IDs of security groups that have too open permissions. e.g. CIDR block is equal to `0.0.0.0/0`(open to the whole world). 68 | 5. Fifth column `EC2 TAGS` contains tags of a given EC2 instance to help you identify purpose of this instance. 69 | 70 | #### Docs 71 | You can find more information about encryption in the following documentation: 72 | 1. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html 73 | 74 | ### S3 Scan 75 | #### How to use 76 | To perform audit on all S3 buckets, type: 77 | ``` 78 | $ cloud-security-audit --service s3 79 | ``` 80 | Cloud Security Audit supports AWS profiles - to specify profile use the flag `-p` or `--profile`. 81 | 82 | #### Example output 83 | 84 | ```bash 85 | +------------------------------+---------+---------+-------------+------------+ 86 | | BUCKET NAME | DEFAULT | LOGGING | ACL | POLICY | 87 | | | | | | | 88 | | | SSE | ENABLED | IS PUBLIC | IS PUBLIC | 89 | | | | | | | 90 | | | | | R - READ | R - READ | 91 | | | | | | | 92 | | | | | W - WRITE | W - WRITE | 93 | | | | | | | 94 | | | | | D - DELETE | D - DELETE | 95 | +------------------------------+---------+---------+-------------+------------+ 96 | | bucket1 | NONE | true | false | false | 97 | +------------------------------+---------+---------+-------------+------------+ 98 | | bucket2 | DKMS | false | false | true [R] | 99 | +------------------------------+---------+---------+-------------+------------+ 100 | | bucket3 | AES256 | false | true [RWD] | false | 101 | +--------------------------- --+---------+---------+-------------+------------+ 102 | ``` 103 | 104 | #### How to read it 105 | 106 | 1. First column `BUCKET NAME` contains names of the s3 buckets. 107 | 2. Second column `DEFAULT SSE` gives you information on which default type of server side encryption was used in your S3 bucket: 108 | * `NONE` - Default SSE not enabled. 109 | * `DKMS` - Default SSE enabled, AWS KMS Key used to encrypt data. 110 | * `AES256` - Default SSE enabled, [AES256](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html). 111 | 3. Third column `LOGGING ENABLED` contains information if Server access logging was enabled for a given S3 bucket. This provides detailed records for the requests that are made to an S3 bucket. More information about Server Access Logging can be found [here](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/server-access-logging.html) 112 | 4. Fourth column `ACL IS PUBLIC` provides information if ACL (Access Control List) contains permissions, that make the bucket public (allow read/writes for anyone). More information about ACLs [here](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html) 113 | 5. Fifth column `POLICY IS PUBLIC` contains information if bucket's policy allows any action (read/write) for an anonymous user. More about bucket policies [here](https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html) 114 | R, W and D letters describe what type of action is available for everyone. 115 | #### Docs 116 | You can find more about securing your S3's in the following documentations: 117 | 1. https://docs.aws.amazon.com/AmazonS3/latest/dev/serv-side-encryption.html 118 | 2. https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerLogs.html 119 | 3. https://docs.aws.amazon.com/AmazonS3/latest/user-guide/server-access-logging.html 120 | 121 | ## License 122 | 123 | [Apache License 2.0](LICENSE) 124 | 125 | ## Maintainers 126 | 127 | - [Michał Połcik](https://github.com/mwpolcik) 128 | - [Maksymilian Wojczuk](https://github.com/maxiwoj) 129 | - [Piotr Figwer](https://github.com/pfigwer) 130 | - [Sylwia Gargula](https://github.com/SylwiaGargula) 131 | - [Mateusz Piwowarczyk](https://github.com/piwowarc) 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Appliscale 190 | 191 | Maintainers and Contributors: 192 | 193 | - Michał Połcik (michal.polcik@appliscale.io) 194 | - Maksymilian Wojczuk (max.wojczuk@appliscale.io) 195 | - Piotr Figwer (piotr.figwer@appliscale.io) 196 | - Sylwia Gargula (sylwia.gargula@appliscale.io) 197 | - Mateusz Piwowarczyk (mateusz.piwowarczyk@appliscale.io) 198 | 199 | Licensed under the Apache License, Version 2.0 (the "License"); 200 | you may not use this file except in compliance with the License. 201 | You may obtain a copy of the License at 202 | 203 | http://www.apache.org/licenses/LICENSE-2.0 204 | 205 | Unless required by applicable law or agreed to in writing, software 206 | distributed under the License is distributed on an "AS IS" BASIS, 207 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 208 | See the License for the specific language governing permissions and 209 | limitations under the License. 210 | --------------------------------------------------------------------------------