Skip to content

Commit

Permalink
⭐️ add aws-ecs id-detector, ensure the iddetectors get passed thru (#880
Browse files Browse the repository at this point in the history
)

adding aws-ecs id-detector to allow for ecs container id detection when
running a local scan on an ecs container.

also, it looks like we dropped support for passing thru the
user-id-detector on the local scan path at least at some point, it's
back in there now

`scan local --id-detector aws-ecs`
  • Loading branch information
vjeffrey committed Feb 7, 2023
1 parent c9c24dd commit 2f5ff7a
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 5 deletions.
3 changes: 2 additions & 1 deletion apps/cnquery/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ func getCobraScanConfig(cmd *cobra.Command, args []string, provider providers.Pr

// determine the scan config from pipe or args
flagAsset := builder.ParseTargetAsset(cmd, args, provider, assetType)
iddetector, _ := cmd.Flags().GetString("id-detector")
flagAsset.IdDetector = []string{iddetector}
conf.Inventory, err = inventoryloader.ParseOrUse(flagAsset, viper.GetBool("insecure"))
if err != nil {
return nil, errors.Wrap(err, "could not load configuration")
Expand Down Expand Up @@ -454,7 +456,6 @@ func getCobraScanConfig(cmd *cobra.Command, args []string, provider providers.Pr
if conf.DoRecord {
log.Info().Msg("enable recording of platform calls")
}

return &conf, nil
}

Expand Down
3 changes: 2 additions & 1 deletion motor/discovery/aws/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"go.mondoo.com/cnquery/motor/asset"
"go.mondoo.com/cnquery/motor/discovery/common"
"go.mondoo.com/cnquery/motor/motorid/awsec2"
awsecsid "go.mondoo.com/cnquery/motor/motorid/awsecs"
"go.mondoo.com/cnquery/motor/motorid/containerid"
"go.mondoo.com/cnquery/motor/platform"
"go.mondoo.com/cnquery/motor/providers"
Expand Down Expand Up @@ -191,7 +192,7 @@ func ecsContainerToAsset(ctx context.Context, account string, region string, c e

asset := &asset.Asset{
Name: *c.Name,
PlatformIds: []string{containerid.MondooContainerID(*c.RuntimeId)},
PlatformIds: []string{containerid.MondooContainerID(*c.RuntimeId), awsecsid.MondooECSContainerID(*c.ContainerArn)},
Platform: &platform.Platform{
Kind: providers.Kind_KIND_CONTAINER,
Runtime: providers.RUNTIME_AWS_ECS,
Expand Down
1 change: 0 additions & 1 deletion motor/motor.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func New(provider providers.Instance, motorOpts ...MotorOption) (*Motor, error)
for i := range motorOpts {
motorOpts[i](m)
}

// set the detector after the opts have been applied to ensure its going via the recorder
// if activated
_, ok := m.Provider.(*local.Provider)
Expand Down
48 changes: 48 additions & 0 deletions motor/motorid/awsecs/awsecs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package awsecsid

import (
"fmt"

"github.com/aws/aws-sdk-go/aws/arn"
"github.com/cockroachdb/errors"
"go.mondoo.com/cnquery/motor/platform"
"go.mondoo.com/cnquery/motor/providers/local"
"go.mondoo.com/cnquery/motor/providers/mock"
"go.mondoo.com/cnquery/motor/providers/os"
)

func MondooECSContainerID(containerArn string) string {
var account, region, id string
if arn.IsARN(containerArn) {
if p, err := arn.Parse(containerArn); err == nil {
account = p.AccountID
region = p.Region
id = p.Resource
}
}
return "//platformid.api.mondoo.app/runtime/aws/ecs/v1/accounts/" + account + "/regions/" + region + "/" + id
}

type Identity struct {
ContainerArn string
Name string
RuntimeID string
PlatformIds []string
AccountPlatformID string
}
type InstanceIdentifier interface {
Identify() (Identity, error)
}

func Resolve(provider os.OperatingSystemProvider, pf *platform.Platform) (InstanceIdentifier, error) {
_, ok := provider.(*local.Provider)
if ok {
return NewContainerMetadata(provider, pf), nil
}
_, ok = provider.(*mock.Provider)
if ok {
return NewContainerMetadata(provider, pf), nil
}

return nil, errors.New(fmt.Sprintf("awsecs id detector is not supported for your asset: %s %s", pf.Name, pf.Version))
}
80 changes: 80 additions & 0 deletions motor/motorid/awsecs/metadata_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package awsecsid

import (
"encoding/json"
"io/ioutil"
"strings"

"github.com/aws/aws-sdk-go/aws/arn"
"github.com/cockroachdb/errors"
"github.com/rs/zerolog/log"
"go.mondoo.com/cnquery/motor/motorid/containerid"
"go.mondoo.com/cnquery/motor/platform"
"go.mondoo.com/cnquery/motor/providers/os"
)

const (
identityUrl = "${ECS_CONTAINER_METADATA_URI_V4}"
)

func NewContainerMetadata(provider os.OperatingSystemProvider, pf *platform.Platform) *ContainerMetadata {
return &ContainerMetadata{
provider: provider,
platform: pf,
}
}

type ContainerMetadata struct {
provider os.OperatingSystemProvider
platform *platform.Platform
}

func (m *ContainerMetadata) Identify() (Identity, error) {
log.Debug().Msg("getting ecs container identity")

containerDocument, err := m.containerIdentityDocument()
if err != nil {
return Identity{}, err
}
// parse into struct
doc := EcrContainerIdentityDoc{}
if err := json.NewDecoder(strings.NewReader(containerDocument)).Decode(&doc); err != nil {
return Identity{}, errors.Wrap(err, "failed to decode ECS container identity document")
}
var accountID string
if arn.IsARN(doc.ContainerArn) {
if p, err := arn.Parse(doc.ContainerArn); err == nil {
accountID = p.AccountID
}
}
return Identity{
Name: doc.Name,
ContainerArn: doc.ContainerArn,
RuntimeID: doc.DockerId,
AccountPlatformID: "//platformid.api.mondoo.app/runtime/aws/accounts/" + accountID,
PlatformIds: []string{MondooECSContainerID(doc.ContainerArn), containerid.MondooContainerID(doc.DockerId)},
}, nil
}

func (m *ContainerMetadata) curlDocument(url string) (string, error) {
cmd, err := m.provider.RunCommand("curl " + url)
if err != nil {
return "", err
}
data, err := ioutil.ReadAll(cmd.Stdout)
if err != nil {
return "", err
}

return strings.TrimSpace(string(data)), nil
}

func (m *ContainerMetadata) containerIdentityDocument() (string, error) {
return m.curlDocument(identityUrl)
}

type EcrContainerIdentityDoc struct {
DockerId string `json:"DockerId"`
Name string `json:"Name"`
ContainerArn string `json:"ContainerARN"`
}
31 changes: 31 additions & 0 deletions motor/motorid/awsecs/metadata_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package awsecsid

import (
"testing"

"github.com/stretchr/testify/require"
"go.mondoo.com/cnquery/motor"
"go.mondoo.com/cnquery/motor/providers/mock"
)

func TestEC2RoleProviderInstanceIdentityUnix(t *testing.T) {
provider, err := mock.NewFromTomlFile("./testdata/container-identity.toml")
require.NoError(t, err)

m, err := motor.New(provider)
require.NoError(t, err)

p, err := m.Platform()
require.NoError(t, err)

metadata := NewContainerMetadata(provider, p)
ident, err := metadata.Identify()

require.Nil(t, err)
require.Equal(t, "fargate-app", ident.Name)
require.Equal(t, "arn:aws:ecs:us-east-1:172746783610:container/vjtest/f088b38d61ac45d6a946b5aebbe7197a/314e35e0-2d0a-4408-b37e-16063461d73a", ident.ContainerArn)
require.Equal(t, "f088b38d61ac45d6a946b5aebbe7197a-3681984407", ident.RuntimeID)
require.Contains(t, ident.PlatformIds, "//platformid.api.mondoo.app/runtime/docker/containers/f088b38d61ac45d6a946b5aebbe7197a-3681984407")
require.Contains(t, ident.PlatformIds, "//platformid.api.mondoo.app/runtime/aws/ecs/v1/accounts/172746783610/regions/us-east-1/container/vjtest/f088b38d61ac45d6a946b5aebbe7197a/314e35e0-2d0a-4408-b37e-16063461d73a")
require.Contains(t, ident.AccountPlatformID, "//platformid.api.mondoo.app/runtime/aws/accounts/172746783610")
}
31 changes: 31 additions & 0 deletions motor/motorid/awsecs/testdata/container-identity.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[commands."uname -s"]
stdout = "Linux"

[commands."uname -m"]
stdout = "x86_64"

[commands."uname -r"]
stdout = "4.9.125-linuxkit"

[files."/etc/redhat-release"]
content = "Red Hat Enterprise Linux Server release 7.2 (Maipo)"

[commands."curl ${ECS_CONTAINER_METADATA_URI_V4}"]
stdout = """
{
"DockerId":"f088b38d61ac45d6a946b5aebbe7197a-3681984407",
"Name":"fargate-app",
"DockerName":"fargate-app",
"Image":"public.ecr.aws/docker/library/httpd:latest",
"ImageID":"sha256:87a012bf99bf5e3e0f628ac1f69abbeab534282857fba3a359ca3a3f4a02429a",
"Labels":{"com.amazonaws.ecs.cluster":"arn:aws:ecs:us-east-1:172746783610:cluster/vjtest","com.amazonaws.ecs.container-name":"fargate-app","com.amazonaws.ecs.task-arn":"arn:aws:ecs:us-east-1:172746783610:task/vjtest/f088b38d61ac45d6a946b5aebbe7197a","com.amazonaws.ecs.task-definition-family":"sample-fargate","com.amazonaws.ecs.task-definition-version":"2"},
"DesiredStatus":"RUNNING",
"KnownStatus":"RUNNING",
"Limits":{"CPU":2},
"CreatedAt":"2023-01-31T06:19:11.226060573Z",
"StartedAt":"2023-01-31T06:19:11.226060573Z",
"Type":"NORMAL",
"Networks":[{"NetworkMode":"awsvpc","IPv4Addresses":["172.31.12.124"],"AttachmentIndex":0,"MACAddress":"02:ee:fc:59:ac:5f","IPv4SubnetCIDRBlock":"172.31.0.0/20","DomainNameServers":["172.31.0.2"],"DomainNameSearchList":["ec2.internal"],"PrivateDNSName":"ip-172-31-12-124.ec2.internal","SubnetGatewayIpv4Address":"172.31.0.1/20"}],
"ContainerARN":"arn:aws:ecs:us-east-1:172746783610:container/vjtest/f088b38d61ac45d6a946b5aebbe7197a/314e35e0-2d0a-4408-b37e-16063461d73a"
}
"""
20 changes: 18 additions & 2 deletions motor/motorid/clouddetect/providers/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/afero"
"go.mondoo.com/cnquery/motor/motorid/awsec2"
awsecsid "go.mondoo.com/cnquery/motor/motorid/awsecs"
"go.mondoo.com/cnquery/motor/platform"
"go.mondoo.com/cnquery/motor/providers/os"
"go.mondoo.com/cnquery/resources/packs/os/smbios"
Expand Down Expand Up @@ -57,15 +58,30 @@ func Detect(provider os.OperatingSystemProvider, p *platform.Platform) (string,
return "", "", nil
}
id, err := mdsvc.Identify()
if err == nil {
return id.InstanceID, id.InstanceName, []string{id.AccountID}
}
log.Debug().Err(err).
Str("transport", provider.Kind().String()).
Strs("platform", p.GetFamily()).
Msg("failed to get aws platform id")
// try ecs
mdsvcEcs, err := awsecsid.Resolve(provider, p)
if err != nil {
log.Debug().Err(err).Msg("failed to get metadata resolver")
return "", "", nil
}
idEcs, err := mdsvcEcs.Identify()
if err == nil {
return idEcs.PlatformIds[0], idEcs.Name, []string{idEcs.AccountPlatformID}
} else {
log.Debug().Err(err).
Str("transport", provider.Kind().String()).
Strs("platform", p.GetFamily()).
Msg("failed to get aws platform id")
return "", "", nil
}
return id.InstanceID, id.InstanceName, []string{id.AccountID}
}
}

return "", "", nil
}
18 changes: 18 additions & 0 deletions motor/motorid/clouddetect/providers/aws/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,21 @@ func TestDetectNotInstance(t *testing.T) {

require.Len(t, related, 0)
}

func TestDetectConainer(t *testing.T) {
provider, err := mock.NewFromTomlFile("./testdata/container.toml")
require.NoError(t, err)

m, err := motor.New(provider)
require.NoError(t, err)

p, err := m.Platform()
require.NoError(t, err)

identifier, name, related := Detect(provider, p)

assert.Equal(t, "//platformid.api.mondoo.app/runtime/aws/ecs/v1/accounts/172746783610/regions/us-east-1/container/vjtest/f088b38d61ac45d6a946b5aebbe7197a/314e35e0-2d0a-4408-b37e-16063461d73a", identifier)
assert.Equal(t, "fargate-app", name)
require.Len(t, related, 1)
assert.Equal(t, "//platformid.api.mondoo.app/runtime/aws/accounts/172746783610", related[0])
}
36 changes: 36 additions & 0 deletions motor/motorid/clouddetect/providers/aws/testdata/container.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[commands."uname -s"]
stdout = "Linux"

[commands."uname -m"]
stdout = "x86_64"

[commands."uname -r"]
stdout = "4.14.301-224.520.amzn2.x86_64t"

[files."/etc/os-release"]
content = "PRETTY_NAME='Debian GNU/Linux 11 (bullseye)' NAME='Debian GNU/Linux' VERSION_ID='11' VERSION='11 (bullseye)' VERSION_CODENAME=bullseye ID=debian HOME_URL='https://www.debian.org/' SUPPORT_URL='https://www.debian.org/support' BUG_REPORT_URL='https://bugs.debian.org/'"

[files."/sys/class/dmi/id/bios_vendor"]
path = "/sys/class/dmi/id/bios_vendor"
enoent = false
content = "Amazon EC2"

[commands."curl ${ECS_CONTAINER_METADATA_URI_V4}"]
stdout = """
{
"DockerId":"f088b38d61ac45d6a946b5aebbe7197a-3681984407",
"Name":"fargate-app",
"DockerName":"fargate-app",
"Image":"public.ecr.aws/docker/library/httpd:latest",
"ImageID":"sha256:87a012bf99bf5e3e0f628ac1f69abbeab534282857fba3a359ca3a3f4a02429a",
"Labels":{"com.amazonaws.ecs.cluster":"arn:aws:ecs:us-east-1:172746783610:cluster/vjtest","com.amazonaws.ecs.container-name":"fargate-app","com.amazonaws.ecs.task-arn":"arn:aws:ecs:us-east-1:172746783610:task/vjtest/f088b38d61ac45d6a946b5aebbe7197a","com.amazonaws.ecs.task-definition-family":"sample-fargate","com.amazonaws.ecs.task-definition-version":"2"},
"DesiredStatus":"RUNNING",
"KnownStatus":"RUNNING",
"Limits":{"CPU":2},
"CreatedAt":"2023-01-31T06:19:11.226060573Z",
"StartedAt":"2023-01-31T06:19:11.226060573Z",
"Type":"NORMAL",
"Networks":[{"NetworkMode":"awsvpc","IPv4Addresses":["172.31.12.124"],"AttachmentIndex":0,"MACAddress":"02:ee:fc:59:ac:5f","IPv4SubnetCIDRBlock":"172.31.0.0/20","DomainNameServers":["172.31.0.2"],"DomainNameSearchList":["ec2.internal"],"PrivateDNSName":"ip-172-31-12-124.ec2.internal","SubnetGatewayIpv4Address":"172.31.0.1/20"}],
"ContainerARN":"arn:aws:ecs:us-east-1:172746783610:container/vjtest/f088b38d61ac45d6a946b5aebbe7197a/314e35e0-2d0a-4408-b37e-16063461d73a"
}
"""
18 changes: 18 additions & 0 deletions motor/motorid/motorid.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/rs/zerolog/log"
"go.mondoo.com/cnquery/motor/discovery/aws"
"go.mondoo.com/cnquery/motor/motorid/awsec2"
awsecsid "go.mondoo.com/cnquery/motor/motorid/awsecs"
"go.mondoo.com/cnquery/motor/motorid/clouddetect"
"go.mondoo.com/cnquery/motor/motorid/hostname"
"go.mondoo.com/cnquery/motor/motorid/machineid"
Expand Down Expand Up @@ -165,6 +166,23 @@ func GatherPlatformInfo(provider providers.Instance, pf *platform.Platform, idDe
}, nil
}
return &PlatformInfo{}, nil
case isOSProvider && idDetector == providers.AWSEcsDetector:
metadata, err := awsecsid.Resolve(osProvider, pf)
if err != nil {
return nil, err
}
ident, err := metadata.Identify()
if err != nil {
return nil, err
}
if len(ident.PlatformIds) != 0 {
return &PlatformInfo{
IDs: ident.PlatformIds,
Name: ident.Name,
RelatedPlatformIDs: []string{ident.AccountPlatformID},
}, nil
}
return &PlatformInfo{}, nil
case isOSProvider && idDetector == providers.CloudDetector:
identifier, name, relatedIdentifiers := clouddetect.Detect(osProvider, pf)
if identifier != "" {
Expand Down
1 change: 1 addition & 0 deletions motor/providers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
MachineIdDetector PlatformIdDetector = "machine-id"
CloudDetector PlatformIdDetector = "cloud-detect"
AWSEc2Detector PlatformIdDetector = "aws-ec2"
AWSEcsDetector PlatformIdDetector = "aws-ecs"
SshHostKey PlatformIdDetector = "ssh-host-key"
// TransportPlatformIdentifierDetector is a detector that gets the platform id
// from the transports Identifier() method. This requires the
Expand Down

0 comments on commit 2f5ff7a

Please sign in to comment.