Skip to content

Commit

Permalink
Allow aws checks to use existing credentials without assumerole
Browse files Browse the repository at this point in the history
  • Loading branch information
jesusfcr committed Jan 20, 2025
1 parent 855d111 commit e9498f3
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 360 deletions.
96 changes: 18 additions & 78 deletions cmd/vulcan-aws-alerts/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,104 +5,44 @@ Copyright 2020 Adevinta
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"

"github.com/aws/aws-sdk-go/aws/credentials"

check "github.com/adevinta/vulcan-check-sdk"
"github.com/adevinta/vulcan-check-sdk/helpers"
checkstate "github.com/adevinta/vulcan-check-sdk/state"
"github.com/adevinta/vulcan-checks/internal/awshelpers"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go/aws/arn"
)

var (
checkName = "vulcan-aws-alerts"
logger = check.NewCheckLog(checkName)
)
var checkName = "vulcan-aws-alerts"

func main() {
run := func(ctx context.Context, target, assetType, optJSON string, state checkstate.State) error {
if target == "" {
return fmt.Errorf("check target missing")
}
logger := check.NewCheckLog(checkName)

vulcanAssumeRoleEndpoint := os.Getenv("VULCAN_ASSUME_ROLE_ENDPOINT")
if vulcanAssumeRoleEndpoint == "" {
return fmt.Errorf("VULCAN_ASSUME_ROLE_ENDPOINT option is missing")
}
roleName := os.Getenv("ROLE_NAME")

isReachable, err := helpers.IsReachable(target, assetType,
helpers.NewAWSCreds(vulcanAssumeRoleEndpoint, roleName))
parsedARN, err := arn.Parse(target)
if err != nil {
logger.Warnf("Can not check asset reachability: %v", err)
}
if !isReachable {
return checkstate.ErrAssetUnreachable
return fmt.Errorf("unable to parse ARN: %w", err)
}
assumeRoleEndpoint := os.Getenv("VULCAN_ASSUME_ROLE_ENDPOINT")
roleName := os.Getenv("VULCAN_ASSUME_ROLE_ENDPOINT")

parsedARN, err := arn.Parse(target)
var cfg aws.Config
if assumeRoleEndpoint == "" {
cfg, err = awshelpers.GetAwsConfig(target, roleName, 3600)
} else {
cfg, err = awshelpers.GetAwsConfigWithVulcanAssumeRole(assumeRoleEndpoint, parsedARN.AccountID, roleName, 3600)

}
if err != nil {
return err
logger.Errorf("unable to get AWS config: %v", err)
return checkstate.ErrAssetUnreachable
}

return caCertificateRotation(parsedARN.AccountID, vulcanAssumeRoleEndpoint, roleName, state)
return caCertificateRotation(logger, cfg, parsedARN.AccountID, state)
}
c := check.NewCheckFromHandler(checkName, run)
c.RunAndServe()
}

// AssumeRoleResponse represent a response from vulcan-assume-role
type AssumeRoleResponse struct {
AccessKey string `json:"access_key"`
SecretAccessKey string `json:"secret_access_key"`
SessionToken string `json:"session_token"`
}

func getCredentials(url string, accountID, role string) (*credentials.Credentials, error) {
m := map[string]string{"account_id": accountID}
if role != "" {
m["role"] = role
}
jsonBody, err := json.Marshal(m)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Errorf("cannot do request: %s", err.Error())
return nil, err
}
defer resp.Body.Close()

assumeRoleResponse := AssumeRoleResponse{}
buf, err := io.ReadAll(resp.Body)
if err != nil {
logger.Errorf("Cannot read request body %s", err.Error())
return nil, err
}

err = json.Unmarshal(buf, &assumeRoleResponse)
if err != nil {
logger.Errorf("Cannot decode request %s", err.Error())
logger.Errorf("RequestBody: %s", string(buf))
return nil, err
}

return credentials.NewStaticCredentials(
assumeRoleResponse.AccessKey,
assumeRoleResponse.SecretAccessKey,
assumeRoleResponse.SessionToken), nil
}
91 changes: 40 additions & 51 deletions cmd/vulcan-aws-alerts/rds.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,27 @@ import (
"fmt"

report "github.com/adevinta/vulcan-report"
"github.com/sirupsen/logrus"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/rds"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/rds"

"github.com/adevinta/vulcan-check-sdk/helpers"
"github.com/adevinta/vulcan-check-sdk/state"
)

func caCertificateRotation(target string, vulcanAssumeRoleEndpoint string, roleName string, state state.State) error {
sess, err := session.NewSession(&aws.Config{})
if err != nil {
return err
}

creds, err := getCredentials(vulcanAssumeRoleEndpoint, target, roleName)
if err != nil {
return err
}

func caCertificateRotation(logger *logrus.Entry, cfg aws.Config, target string, state state.State) error {
// Iterate over all AWS regions where RDS is available
// TODO: Looks there is no replacemente for this on aws-sdk-go-v2.
var err error
for region := range endpoints.AwsPartition().Services()[endpoints.RdsServiceID].Regions() {
sess.Config.Region = aws.String(region)
s := rds.New(sess, &aws.Config{Credentials: creds})
cfg.Region = region
rdsClient := rds.NewFromConfig(cfg)

ctx := context.Background()
logger.Info(fmt.Sprintf("Describing pending maintenance actions on RDS for %s region", region))
result, err := s.DescribePendingMaintenanceActionsWithContext(ctx, &rds.DescribePendingMaintenanceActionsInput{})
result, err := rdsClient.DescribePendingMaintenanceActions(ctx, &rds.DescribePendingMaintenanceActionsInput{})
if err != nil {
logger.Error(err)
continue
Expand All @@ -49,52 +41,49 @@ func caCertificateRotation(target string, vulcanAssumeRoleEndpoint string, roleN
for _, action := range result.PendingMaintenanceActions {
for _, details := range action.PendingMaintenanceActionDetails {
if *details.Action == "ca-certificate-rotation" {
result, err := s.DescribeDBInstancesWithContext(
result, err := rdsClient.DescribeDBInstances(
ctx,
&rds.DescribeDBInstancesInput{
DBInstanceIdentifier: action.ResourceIdentifier,
})
if err != nil {
logger.Error(err)
logger.Info(err)
continue
}

for _, instance := range result.DBInstances {
if instance != nil {
state.AddVulnerabilities(
report.Vulnerability{
AffectedResource: aws.StringValue(instance.DBInstanceArn),
Labels: []string{"issue"},
Fingerprint: helpers.ComputeFingerprint(details.AutoAppliedAfterDate),
Summary: `Managed AWS databases using CA about to expire`,
Score: report.SeverityThresholdHigh,
Description: `Due to the expiration of the AWS RDS CA, and to prevent downtime ` +
`in your applications, you should add the new CA to your clients using a ` +
`managed (i.e. RDS or Aurora) database through SSL/TLS and perform maintenance ` +
`on the affected database instances before the certificate expiration date.`,
References: []string{"https://aws.amazon.com/blogs/database/amazon-rds-customers-update-your-ssl-tls-certificates-by-february-5-2020/"},
Resources: []report.ResourcesGroup{
{
Name: `Instances`,
Header: []string{"Identifier", "Account", "Region", "DBName", "Engine", "ARN", "AutoAppliedAfterDate", "CurrentApplyDate"},
Rows: []map[string]string{
{
"AutoAppliedAfterDate": aws.TimeValue(details.AutoAppliedAfterDate).String(),
"CurrentApplyDate": aws.TimeValue(details.CurrentApplyDate).String(),
"Identifier": aws.StringValue(instance.DBInstanceIdentifier),
"Account": target,
"Region": region,
"DBName": aws.StringValue(instance.DBName),
"Engine": aws.StringValue(instance.Engine),
"ARN": aws.StringValue(instance.DBInstanceArn),
},
state.AddVulnerabilities(
report.Vulnerability{
AffectedResource: aws.ToString(instance.DBInstanceArn),
Labels: []string{"issue"},
Fingerprint: helpers.ComputeFingerprint(details.AutoAppliedAfterDate),
Summary: `Managed AWS databases using CA about to expire`,
Score: report.SeverityThresholdHigh,
Description: `Due to the expiration of the AWS RDS CA, and to prevent downtime ` +
`in your applications, you should add the new CA to your clients using a ` +
`managed (i.e. RDS or Aurora) database through SSL/TLS and perform maintenance ` +
`on the affected database instances before the certificate expiration date.`,
References: []string{"https://aws.amazon.com/blogs/database/amazon-rds-customers-update-your-ssl-tls-certificates-by-february-5-2020/"},
Resources: []report.ResourcesGroup{
{
Name: `Instances`,
Header: []string{"Identifier", "Account", "Region", "DBName", "Engine", "ARN", "AutoAppliedAfterDate", "CurrentApplyDate"},
Rows: []map[string]string{
{
"AutoAppliedAfterDate": aws.ToTime(details.AutoAppliedAfterDate).String(),
"CurrentApplyDate": aws.ToTime(details.CurrentApplyDate).String(),
"Identifier": aws.ToString(instance.DBInstanceIdentifier),
"Account": target,
"Region": region,
"DBName": aws.ToString(instance.DBName),
"Engine": aws.ToString(instance.Engine),
"ARN": aws.ToString(instance.DBInstanceArn),
},
},
},
})
} else {
logger.Warn("Received nil instance from DescribeDBInstancesWithContext")
}
},
})

}
}
}
Expand Down
100 changes: 12 additions & 88 deletions cmd/vulcan-aws-trusted-advisor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ Copyright 2019 Adevinta
package main

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
Expand All @@ -19,14 +16,12 @@ import (
check "github.com/adevinta/vulcan-check-sdk"
"github.com/adevinta/vulcan-check-sdk/helpers"
checkstate "github.com/adevinta/vulcan-check-sdk/state"
"github.com/adevinta/vulcan-checks/internal/awshelpers"
report "github.com/adevinta/vulcan-report"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/aws-sdk-go-v2/service/support"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -106,43 +101,23 @@ func extractLinesFromHTML(htmlText string) []string {
}

func scanAccount(opt options, target, _ string, logger *logrus.Entry, state checkstate.State) error {
assumeRoleEndpoint := os.Getenv("VULCAN_ASSUME_ROLE_ENDPOINT")
role := os.Getenv("ROLE_NAME")

parsedARN, err := arn.Parse(target)
if err != nil {
return err
return fmt.Errorf("unable to parse ARN: %w", err)
}
assumeRoleEndpoint := os.Getenv("VULCAN_ASSUME_ROLE_ENDPOINT")
roleName := os.Getenv("VULCAN_ASSUME_ROLE_ENDPOINT")

var cfg aws.Config
if assumeRoleEndpoint != "" {
creds, err := getCredentials(assumeRoleEndpoint, parsedARN.AccountID, role, logger)
if err != nil {
if errors.Is(err, errNoCredentials) {
return checkstate.ErrAssetUnreachable
}
return err
}
credsProvider := credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken)
cfg, err = config.LoadDefaultConfig(context.Background(),
config.WithRegion("us-east-1"),
config.WithCredentialsProvider(credsProvider),
)
if err != nil {
return fmt.Errorf("unable to create AWS config: %w", err)
}
if assumeRoleEndpoint == "" {
cfg, err = awshelpers.GetAwsConfig(target, roleName, 3600)
} else {
// try to access with the default credentials
cfg, err = config.LoadDefaultConfig(context.Background(), config.WithRegion("us-east-1"))
if err != nil {
return fmt.Errorf("unable to create AWS config: %w", err)
}
}
cfg, err = awshelpers.GetAwsConfigWithVulcanAssumeRole(assumeRoleEndpoint, parsedARN.AccountID, roleName, 3600)

// Validate that the account id in the target ARN matches the account id in the credentials
if req, err := sts.NewFromConfig(cfg).GetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{}); err != nil {
return fmt.Errorf("unable to get caller identity: %w", err)
} else if *req.Account != parsedARN.AccountID {
return fmt.Errorf("account id in target ARN does not match the account id in the credentials (target ARN: %s, credentials account id: %s)", parsedARN.AccountID, *req.Account)
}
if err != nil {
logger.Errorf("unable to get AWS config: %v", err)
return checkstate.ErrAssetUnreachable
}

s := support.NewFromConfig(cfg)
Expand Down Expand Up @@ -398,57 +373,6 @@ func scanAccount(opt options, target, _ string, logger *logrus.Entry, state chec
return err
}

// AssumeRoleResponse represent a response from vulcan-assume-role
type AssumeRoleResponse struct {
AccessKey string `json:"access_key"`
SecretAccessKey string `json:"secret_access_key"`
SessionToken string `json:"session_token"`
}

var errNoCredentials = errors.New("unable to decode credentials")

func getCredentials(url string, accountID, role string, logger *logrus.Entry) (*aws.Credentials, error) {
m := map[string]any{"account_id": accountID, "duration": 3600}
if role != "" {
m["role"] = role
}
jsonBody, err := json.Marshal(m)
if err != nil {
return nil, fmt.Errorf("unable to marshal assume role request body for account %s: %w", accountID, err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("unable to create request for the assume role service: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Errorf("cannot do request: %s", err.Error())
return nil, err
}
defer resp.Body.Close() // nolint

assumeRoleResponse := AssumeRoleResponse{}
buf, err := io.ReadAll(resp.Body)
if err != nil {
logger.Errorf("can not read request body %s", err.Error())
return nil, err
}

err = json.Unmarshal(buf, &assumeRoleResponse)
if err != nil {
logger.Errorf("Cannot decode request: %s", err.Error())
logger.Errorf("ResponseBody: %s", string(buf))
return nil, errNoCredentials
}
return &aws.Credentials{
AccessKeyID: assumeRoleResponse.AccessKey,
SecretAccessKey: assumeRoleResponse.SecretAccessKey,
SessionToken: assumeRoleResponse.SessionToken,
}, nil
}

// accountAlias gets one of the current aliases of the account that the
// credentials passed belong to.
func accountAlias(cfg aws.Config) (string, error) {
Expand Down
Loading

0 comments on commit e9498f3

Please sign in to comment.