Skip to content

Commit 89dc589

Browse files
authored
Merge pull request #8 from rpetrich/s3-bucket-scanning
Support S3 bucket scanning
2 parents 64ef884 + fda3a8e commit 89dc589

File tree

5 files changed

+183
-33
lines changed

5 files changed

+183
-33
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ All you need is an AWS account and the ability to create an AWS role and EC2 ins
4343
1. Log into your AWS account and access the Identity and Access Management (IAM) service in the AWS Management Console, then choose [**Create Role**](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html) (you can also use the AWS CLI if you prefer)
4444
2. Select **AWS service** for type of trusted entity
4545
3. Select **EC2** as the allowed service and use case, then choose **Next: Permissions**
46-
4. Select the [**AmazonEC2FullAccess**](https://console.aws.amazon.com/iam/home?region=us-east-1#/policies/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonEC2FullAccess) policy or paste [our recommended policy](https://github.com/rpetrich/patrolaroid/tree/main/docs/recommended-iam-policy.md) (with tighter permissions) into [the JSON editor](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create-console.html#access_policies_create-json-editor), then choose **Next: Tags**
46+
4. Select the [**AmazonEC2FullAccess**](https://console.aws.amazon.com/iam/home?region=us-east-1#/policies/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonEC2FullAccess) and[**AmazonS3FullAccess**](https://console.aws.amazon.com/iam/home?region=us-east-1#/policies/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonS3FullAccess) policies or paste [our recommended policy](https://github.com/rpetrich/patrolaroid/tree/main/docs/recommended-iam-policy.md) (with tighter permissions) into [the JSON editor](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create-console.html#access_policies_create-json-editor), then choose **Next: Tags**
4747
5. No tags are needed, so select **Next: Review**
4848
6. Type **Patrolaroid** for the **Role name**
4949
7. Review the role and, if satisfied, choose **Create role**

docs/recommended-iam-policy.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ For individuals comfortable applying [custom IAM policies](https://docs.aws.amaz
1515
"ec2:DeleteVolume",
1616
"ec2:DescribeSnapshots",
1717
"ec2:DescribeVolumes",
18-
"ec2:DetachVolume"
18+
"ec2:DetachVolume",
19+
"s3:ListBuckets",
20+
"s3:ListObjects",
21+
"s3:GetObject"
1922
],
2023
"Effect": "Allow",
2124
"Resource": "*"

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/aws/aws-sdk-go-v2/config v1.3.0
77
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.1.1
88
github.com/aws/aws-sdk-go-v2/service/ec2 v1.7.1
9+
github.com/aws/aws-sdk-go-v2/service/s3 v1.9.0
910
github.com/capsule8/go-yara v1.1.10-0.20210523225711-dafe562e8c6e
1011
github.com/hillu/go-yara/v4 v4.0.6 // indirect
1112
)

go.sum

+6
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.0.0 h1:k7I9E6tyVWBo7H9ffpnxDWudtjau
1010
github.com/aws/aws-sdk-go-v2/internal/ini v1.0.0/go.mod h1:g3XMXuxvqSMUjnsXXp/960152w0wFS4CXVYgQaSVOHE=
1111
github.com/aws/aws-sdk-go-v2/service/ec2 v1.7.1 h1:2I6fU3pLkiGOrSRCn8lcftG9Xw57ucxXzf+rOLTR6PY=
1212
github.com/aws/aws-sdk-go-v2/service/ec2 v1.7.1/go.mod h1:XzzkrryeCoPUd9jxcdDnI2/UmlfIp13nBSpjl2SDSCM=
13+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.1.0 h1:XwqxIO9LtNXznBbEMNGumtLN60k4nVqDpVwVWx3XU/o=
14+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.1.0/go.mod h1:zdjOOy0ojUn3iNELo6ycIHSMCp4xUbycSHfb8PnbbyM=
1315
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.1.1 h1:l7pDLsmOGrnR8LT+3gIv8NlHpUhs7220E457KEC2UM0=
1416
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.1.1/go.mod h1:2+ehJPkdIdl46VCj67Emz/EH2hpebHZtaLdzqg+sWOI=
17+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.3.1 h1:VH1Y4k+IZ5kcRVqSNw7eAkXyfS7k2/ibKjrNtbhYhV4=
18+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.3.1/go.mod h1:IpjxfORBAFfkMM0VEx5gPPnEy6WV4Hk0F/+zb/SUWyw=
19+
github.com/aws/aws-sdk-go-v2/service/s3 v1.9.0 h1:FZ5UL5aiybSJKiJglPT7YMMwc431IgOX5gvlFAzSjzs=
20+
github.com/aws/aws-sdk-go-v2/service/s3 v1.9.0/go.mod h1:zHCjYoODbYRLz/iFicYswq1gRoxBnHvpY5h2Vg3/tJ4=
1521
github.com/aws/aws-sdk-go-v2/service/sso v1.2.1 h1:alpXc5UG7al7QnttHe/9hfvUfitV8r3w0onPpPkGzi0=
1622
github.com/aws/aws-sdk-go-v2/service/sso v1.2.1/go.mod h1:VimPFPltQ/920i1X0Sb0VJBROLIHkDg2MNP10D46OGs=
1723
github.com/aws/aws-sdk-go-v2/service/sts v1.4.1 h1:9Z00tExoaLutWVDmY6LyvIAcKjHetkbdmpRt4JN/FN0=

main.go

+171-31
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"flag"
66
"fmt"
7+
"io"
78
"io/ioutil"
89
"log"
910
"os"
@@ -17,6 +18,7 @@ import (
1718
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
1819
"github.com/aws/aws-sdk-go-v2/service/ec2"
1920
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
21+
"github.com/aws/aws-sdk-go-v2/service/s3"
2022
yara "github.com/capsule8/go-yara"
2123
)
2224

@@ -71,7 +73,8 @@ func run() int {
7173
instanceId := string(instanceIdBytes)
7274
// parse arguments
7375
signaturePathFlag := flag.String("signatures", "./rules", "a path to YARA signatures")
74-
volumeIdsFlag := flag.String("volume-ids", "", "a comma separated list of volume IDs to scan")
76+
volumeIdsFlag := flag.String("volume-ids", "all", "a comma separated list of volume IDs to scan")
77+
bucketIdsFlag := flag.String("bucket-ids", "all", "a comma separated list of bucket IDs to scan")
7578
flag.Parse()
7679
// load YARA
7780
compiler, err := yara.NewCompiler()
@@ -118,45 +121,72 @@ func run() int {
118121
if ruleCount == 0 {
119122
log.Fatalf("no rules to scan files with; place signatures in ./signatures/*.yar")
120123
}
121-
// connect to EC2
122-
client := ec2.NewFromConfig(cfg)
124+
exitCode := 0
123125
dryRun := false
124126
// search for volumes
125-
var volumes []volumeInfo
126-
var volumeIds []string
127127
if *volumeIdsFlag != "" {
128-
volumeIds = strings.Split(*volumeIdsFlag, ",")
129-
}
130-
var nextToken *string
131-
for {
132-
volumesOutput, err := client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{
133-
DryRun: &dryRun,
134-
NextToken: nextToken,
135-
VolumeIds: volumeIds,
136-
})
137-
if err != nil {
138-
log.Fatalf("describe volumes request failed: %v", err)
128+
var volumes []volumeInfo
129+
var volumeIds []string
130+
if *volumeIdsFlag != "all" {
131+
volumeIds = strings.Split(*volumeIdsFlag, ",")
139132
}
140-
for _, volume := range volumesOutput.Volumes {
141-
info := volumeInfo{
142-
VolumeId: *volume.VolumeId,
133+
// connect to EC2
134+
client := ec2.NewFromConfig(cfg)
135+
var nextToken *string
136+
for {
137+
volumesOutput, err := client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{
138+
DryRun: &dryRun,
139+
NextToken: nextToken,
140+
VolumeIds: volumeIds,
141+
})
142+
if err != nil {
143+
log.Fatalf("describe volumes request failed: %v", err)
143144
}
144-
for _, attachment := range volume.Attachments {
145-
info.Attachments = append(info.Attachments, *attachment.InstanceId)
145+
for _, volume := range volumesOutput.Volumes {
146+
info := volumeInfo{
147+
VolumeId: *volume.VolumeId,
148+
}
149+
for _, attachment := range volume.Attachments {
150+
info.Attachments = append(info.Attachments, *attachment.InstanceId)
151+
}
152+
volumes = append(volumes, info)
153+
log.Printf("found volume %s", info.VolumeId)
154+
}
155+
if nextToken = volumesOutput.NextToken; nextToken == nil {
156+
break
146157
}
147-
volumes = append(volumes, info)
148-
log.Printf("found volume %s", info.VolumeId)
149158
}
150-
if nextToken = volumesOutput.NextToken; nextToken == nil {
151-
break
159+
log.Printf("scanning the following volumes: %v", volumes)
160+
for _, volume := range volumes {
161+
if err = processVolume(ctx, client, az, instanceId, volume, r); err != nil {
162+
log.Printf("%v", err)
163+
exitCode = 1
164+
}
152165
}
166+
} else {
167+
log.Printf("skipping scanning volumes, none specified")
153168
}
154-
log.Printf("scanning the following volumes: %v", volumes)
155-
exitCode := 0
156-
for _, volume := range volumes {
157-
if err = processVolume(ctx, client, az, instanceId, volume, r); err != nil {
158-
log.Printf("%v", err)
159-
exitCode = 1
169+
if *bucketIdsFlag != "" {
170+
var bucketIds []string
171+
client := s3.NewFromConfig(cfg)
172+
if *bucketIdsFlag != "all" {
173+
bucketIds = strings.Split(*bucketIdsFlag, ",")
174+
} else {
175+
// connect to S3
176+
bucketsOutput, err := client.ListBuckets(ctx, &s3.ListBucketsInput{})
177+
if err != nil {
178+
log.Fatalf("list buckets request failed: %v", err)
179+
}
180+
for _, bucket := range bucketsOutput.Buckets {
181+
bucketIds = append(bucketIds, *bucket.Name)
182+
}
183+
}
184+
log.Printf("scanning the following buckets: %v", bucketIds)
185+
for _, bucket := range bucketIds {
186+
if err = processBucket(ctx, client, bucket, r); err != nil {
187+
log.Printf("%v", err)
188+
exitCode = 1
189+
}
160190
}
161191
}
162192
return exitCode
@@ -452,12 +482,122 @@ wait_for_volume_detachment:
452482
_, err = client.DeleteVolume(ctx, &ec2.DeleteVolumeInput{
453483
VolumeId: &snapshotVolumeId,
454484
})
485+
log.Printf("finished scanning %v", volumeInfo)
455486
if err != nil {
456487
return fmt.Errorf("delete volume request failed: %v", err)
457488
}
458489
return errorToReturn
459490
}
460491

492+
type s3MemoryIterator struct {
493+
ctx context.Context
494+
client *s3.Client
495+
bucketId string
496+
key string
497+
size int64
498+
offset int64
499+
err error
500+
}
501+
502+
func (i *s3MemoryIterator) First() *yara.MemoryBlock {
503+
i.offset = 0
504+
return i.Next()
505+
}
506+
507+
func (i *s3MemoryIterator) Next() *yara.MemoryBlock {
508+
base := i.offset
509+
chunkSize := i.size - base
510+
if chunkSize == 0 {
511+
return nil
512+
}
513+
if chunkSize > 2*1024*1024 {
514+
chunkSize = 2 * 1024 * 1024
515+
}
516+
i.offset += chunkSize
517+
return &yara.MemoryBlock{
518+
Base: uint64(base),
519+
Size: uint64(chunkSize),
520+
FetchData: func(buf []byte) {
521+
rangeString := fmt.Sprintf("bytes=%d-%d", base, base+chunkSize)
522+
output, err := i.client.GetObject(i.ctx, &s3.GetObjectInput{
523+
Bucket: &i.bucketId,
524+
Key: &i.key,
525+
Range: &rangeString,
526+
})
527+
if err != nil {
528+
i.err = err
529+
} else {
530+
body := output.Body
531+
defer body.Close()
532+
for len(buf) > 0 {
533+
var n int
534+
n, err = body.Read(buf)
535+
buf = buf[n:]
536+
if err != nil {
537+
if err != io.EOF {
538+
i.err = err
539+
}
540+
break
541+
}
542+
}
543+
}
544+
},
545+
}
546+
}
547+
548+
func processBucket(ctx context.Context, client *s3.Client, bucketId string, rules *yara.Rules) error {
549+
var wg sync.WaitGroup
550+
pathsToScan := make(chan *s3MemoryIterator, 1024)
551+
for i := 0; i < 64; i++ {
552+
wg.Add(1)
553+
go func() {
554+
defer wg.Done()
555+
for iterator := range pathsToScan {
556+
// Actually scan the file
557+
var m yara.MatchRules
558+
if err := rules.ScanMemBlocks(iterator, 0, 0, &m); err != nil {
559+
log.Printf("could not scan file in bucket %s at path %q: %v", bucketId, iterator.key, err)
560+
} else if iterator.err != nil {
561+
log.Printf("could not scan file in bucket %s at path %q: %v", bucketId, iterator.key, iterator.err)
562+
} else {
563+
// If we have matches, dispatch an alert
564+
if len(m) != 0 {
565+
for _, match := range m {
566+
log.Printf("file in bucket %s at path %q violated rule %q from %q", bucketId, iterator.key, match.Rule, match.Namespace)
567+
}
568+
}
569+
}
570+
}
571+
}()
572+
}
573+
log.Printf("scanning bucket %s", bucketId)
574+
var continuationToken *string
575+
for {
576+
listObjectsOutput, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
577+
Bucket: &bucketId,
578+
})
579+
if err != nil {
580+
log.Fatalf("describe volumes request failed: %v", err)
581+
}
582+
for _, object := range listObjectsOutput.Contents {
583+
pathsToScan <- &s3MemoryIterator{
584+
ctx: ctx,
585+
client: client,
586+
bucketId: bucketId,
587+
key: *object.Key,
588+
size: object.Size,
589+
}
590+
}
591+
if continuationToken = listObjectsOutput.NextContinuationToken; continuationToken == nil {
592+
break
593+
}
594+
}
595+
close(pathsToScan)
596+
wg.Wait()
597+
log.Printf("finished scanning %s", bucketId)
598+
return nil
599+
}
600+
461601
func main() {
462602
os.Exit(run())
463603
}

0 commit comments

Comments
 (0)