Files
vuls/reporter/s3.go
2024-06-17 17:37:49 +09:00

170 lines
4.6 KiB
Go

package reporter
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"golang.org/x/xerrors"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
)
// S3Writer writes results to S3
type S3Writer struct {
FormatJSON bool
FormatFullText bool
FormatOneLineText bool
FormatList bool
Gzip bool
config.AWSConf
}
func (w S3Writer) getS3() (*s3.Client, error) {
var optFns []func(*awsConfig.LoadOptions) error
if w.S3Endpoint != "" {
optFns = append(optFns, awsConfig.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{URL: w.S3Endpoint}, nil
})))
}
if w.Region != "" {
optFns = append(optFns, awsConfig.WithRegion(w.Region))
}
if w.Profile != "" {
optFns = append(optFns, awsConfig.WithSharedConfigProfile(w.Profile))
}
switch w.CredentialProvider {
case "":
case config.CredentialProviderAnonymous:
optFns = append(optFns, awsConfig.WithCredentialsProvider(aws.AnonymousCredentials{}))
default:
return nil, xerrors.Errorf("CredentialProvider: %s is not supported", w.CredentialProvider)
}
cfg, err := awsConfig.LoadDefaultConfig(context.TODO(), optFns...)
if err != nil {
return nil, xerrors.Errorf("Failed to load config. err: %w", err)
}
return s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = w.S3UsePathStyle }), nil
}
// Write results to S3
// https://docs.aws.amazon.com/en_us/code-library/latest/ug/go_2_s3_code_examples.html
func (w S3Writer) Write(rs ...models.ScanResult) (err error) {
if len(rs) == 0 {
return nil
}
svc, err := w.getS3()
if err != nil {
return xerrors.Errorf("Failed to get s3 client. err: %w", err)
}
if w.FormatOneLineText {
timestr := rs[0].ScannedAt.Format(time.RFC3339)
k := fmt.Sprintf(timestr + "/summary.txt")
text := formatOneLineSummary(rs...)
if err := w.putObject(svc, k, []byte(text), w.Gzip); err != nil {
return err
}
}
for _, r := range rs {
key := r.ReportKeyName()
if w.FormatJSON {
k := key + ".json"
var b []byte
if b, err = json.Marshal(r); err != nil {
return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
}
if err := w.putObject(svc, k, b, w.Gzip); err != nil {
return err
}
}
if w.FormatList {
k := key + "_short.txt"
text := formatList(r)
if err := w.putObject(svc, k, []byte(text), w.Gzip); err != nil {
return err
}
}
if w.FormatFullText {
k := key + "_full.txt"
text := formatFullPlainText(r)
if err := w.putObject(svc, k, []byte(text), w.Gzip); err != nil {
return err
}
}
}
return nil
}
// ErrBucketExistCheck : bucket existence cannot be checked because s3:ListBucket or s3:ListAllMyBuckets is not allowed
var ErrBucketExistCheck = xerrors.New("bucket existence cannot be checked because s3:ListBucket or s3:ListAllMyBuckets is not allowed")
// Validate check the existence of S3 bucket
func (w S3Writer) Validate() error {
svc, err := w.getS3()
if err != nil {
return xerrors.Errorf("Failed to get s3 client. err: %w", err)
}
// s3:ListBucket
_, err = svc.HeadBucket(context.TODO(), &s3.HeadBucketInput{Bucket: aws.String(w.S3Bucket)})
if err == nil {
return nil
}
var nsb *types.NoSuchBucket
if errors.As(err, &nsb) {
return xerrors.Errorf("Failed to find the buckets. profile: %s, region: %s, bucket: %s", w.Profile, w.Region, w.S3Bucket)
}
// s3:ListAllMyBuckets
result, err := svc.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
if err == nil {
if slices.ContainsFunc(result.Buckets, func(b types.Bucket) bool {
return *b.Name == w.S3Bucket
}) {
return nil
}
return xerrors.Errorf("Failed to find the buckets. profile: %s, region: %s, bucket: %s", w.Profile, w.Region, w.S3Bucket)
}
return ErrBucketExistCheck
}
func (w S3Writer) putObject(svc *s3.Client, k string, b []byte, gzip bool) error {
var err error
if gzip {
if b, err = gz(b); err != nil {
return err
}
k += ".gz"
}
putObjectInput := &s3.PutObjectInput{
Bucket: aws.String(w.S3Bucket),
Key: aws.String(path.Join(w.S3ResultsDir, k)),
Body: bytes.NewReader(b),
ServerSideEncryption: types.ServerSideEncryption(w.S3ServerSideEncryption),
}
if _, err := svc.PutObject(context.TODO(), putObjectInput); err != nil {
return xerrors.Errorf("Failed to upload data to %s/%s, err: %w",
w.S3Bucket, path.Join(w.S3ResultsDir, k), err)
}
return nil
}