From 04ff7d4475fe03e99e5b128aee3d384ca1df28a0 Mon Sep 17 00:00:00 2001 From: Catarina Paralta <46568597+paralta@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:38:25 +0000 Subject: [PATCH] refactor(docker): docker provider to provider v2 --- provider/v2/docker/config.go | 57 +++ provider/v2/docker/discoverer/container.go | 123 ++++++ provider/v2/docker/discoverer/discoverer.go | 78 ++++ provider/v2/docker/discoverer/image.go | 109 +++++ provider/v2/docker/estimator/estimator.go | 31 ++ provider/v2/docker/provider.go | 65 +++ provider/v2/docker/scanner/scanner.go | 432 ++++++++++++++++++++ 7 files changed, 895 insertions(+) create mode 100644 provider/v2/docker/config.go create mode 100644 provider/v2/docker/discoverer/container.go create mode 100644 provider/v2/docker/discoverer/discoverer.go create mode 100644 provider/v2/docker/discoverer/image.go create mode 100644 provider/v2/docker/estimator/estimator.go create mode 100644 provider/v2/docker/provider.go create mode 100644 provider/v2/docker/scanner/scanner.go diff --git a/provider/v2/docker/config.go b/provider/v2/docker/config.go new file mode 100644 index 000000000..64cc117a8 --- /dev/null +++ b/provider/v2/docker/config.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker + +import ( + "fmt" + + "github.com/spf13/viper" +) + +const ( + DefaultEnvPrefix = "VMCLARITY_DOCKER" + DefaultHelperImage = "alpine:3.18.2" + DefaultNetworkName = "vmclarity" +) + +type Config struct { + // HelperImage defines helper container image that performs init tasks. + HelperImage string `mapstructure:"helper_image"` + // NetworkName defines the user defined bridge network where we attach the scanner container. + NetworkName string `mapstructure:"network_name"` +} + +func NewConfig() (*Config, error) { + // Avoid modifying the global instance + v := viper.New() + + v.SetEnvPrefix(DefaultEnvPrefix) + v.AllowEmptyEnv(true) + v.AutomaticEnv() + + _ = v.BindEnv("helper_image") + v.SetDefault("helper_image", DefaultHelperImage) + + _ = v.BindEnv("network_name") + v.SetDefault("network_name", DefaultNetworkName) + + config := &Config{} + if err := v.Unmarshal(config); err != nil { + return nil, fmt.Errorf("failed to parse provider configuration. Provider=Docker: %w", err) + } + + return config, nil +} diff --git a/provider/v2/docker/discoverer/container.go b/provider/v2/docker/discoverer/container.go new file mode 100644 index 000000000..5f8ea7778 --- /dev/null +++ b/provider/v2/docker/discoverer/container.go @@ -0,0 +1,123 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discoverer + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/docker/docker/api/types" + containertypes "github.com/docker/docker/api/types/container" + "golang.org/x/sync/errgroup" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/core/log" + "github.com/openclarity/vmclarity/core/to" +) + +func (d *Discoverer) getContainerAssets(ctx context.Context) ([]apitypes.AssetType, error) { + logger := log.GetLoggerFromContextOrDiscard(ctx) + + // List all docker containers + containers, err := d.DockerClient.ContainerList(ctx, containertypes.ListOptions{All: true}) + if err != nil { + return nil, fmt.Errorf("failed to list containers: %w", err) + } + + // Results will be written to assets concurrently + assetMu := sync.Mutex{} + assets := make([]apitypes.AssetType, 0, len(containers)) + + // Process each container in an independent processor goroutine + processGroup, processCtx := errgroup.WithContext(ctx) + for _, container := range containers { + processGroup.Go( + // processGroup expects a function with empty signature, so we use a function + // generator to enable adding arguments. This avoids issues when using loop + // variables in goroutines via shared memory space. + // + // If any processor returns an error, it will stop all processors. + // IDEA: Decide what the acceptance criteria should be (e.g. >= 50% container processed) + func(container types.Container) func() error { + return func() error { + // Get container info + info, err := d.getContainerInfo(processCtx, container.ID) + if err != nil { + logger.Warnf("Failed to get container. id=%v: %v", container.ID, err) + return nil // skip fail + } + + // Convert to asset + asset := apitypes.AssetType{} + err = asset.FromContainerInfo(info) + if err != nil { + return fmt.Errorf("failed to create AssetType from ContainerInfo: %w", err) + } + + // Write to assets + assetMu.Lock() + assets = append(assets, asset) + assetMu.Unlock() + + return nil + } + }(container), + ) + } + + // This will block until all the processors have executed successfully or until + // the first error. If an error is returned by any processors, processGroup will + // cancel execution via processCtx and return that error. + err = processGroup.Wait() + if err != nil { + return nil, fmt.Errorf("failed to process containers: %w", err) + } + + return assets, nil +} + +func (d *Discoverer) getContainerInfo(ctx context.Context, containerID string) (apitypes.ContainerInfo, error) { + // Inspect container + info, err := d.DockerClient.ContainerInspect(ctx, containerID) + if err != nil { + return apitypes.ContainerInfo{}, fmt.Errorf("failed to inspect container: %w", err) + } + + createdAt, err := time.Parse(time.RFC3339, info.Created) + if err != nil { + return apitypes.ContainerInfo{}, fmt.Errorf("failed to parse time: %w", err) + } + + // Get container image info + imageInfo, err := d.getContainerImageInfo(ctx, info.Image) + if err != nil { + return apitypes.ContainerInfo{}, err + } + + containerName := strings.Trim(info.Name, "/") + + return apitypes.ContainerInfo{ + ContainerName: &containerName, + CreatedAt: to.Ptr(createdAt), + ContainerID: containerID, + Image: to.Ptr(imageInfo), + Labels: convertTags(info.Config.Labels), + ObjectType: "ContainerInfo", + }, nil +} diff --git a/provider/v2/docker/discoverer/discoverer.go b/provider/v2/docker/discoverer/discoverer.go new file mode 100644 index 000000000..bc28259a2 --- /dev/null +++ b/provider/v2/docker/discoverer/discoverer.go @@ -0,0 +1,78 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discoverer + +import ( + "context" + + "github.com/docker/docker/client" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/provider" +) + +var _ provider.Discoverer = &Discoverer{} + +type Discoverer struct { + DockerClient *client.Client +} + +func (d *Discoverer) DiscoverAssets(ctx context.Context) provider.AssetDiscoverer { + assetDiscoverer := provider.NewSimpleAssetDiscoverer() + + go func() { + defer close(assetDiscoverer.OutputChan) + + // Get image assets + imageAssets, err := d.getImageAssets(ctx) + if err != nil { + assetDiscoverer.Error = provider.FatalErrorf("failed to get images. Provider=%s: %w", apitypes.Docker, err) + return + } + + // Get container assets + containerAssets, err := d.getContainerAssets(ctx) + if err != nil { + assetDiscoverer.Error = provider.FatalErrorf("failed to get containers. Provider=%s: %w", apitypes.Docker, err) + return + } + + // Combine assets + assets := append(imageAssets, containerAssets...) + + for _, asset := range assets { + select { + case assetDiscoverer.OutputChan <- asset: + case <-ctx.Done(): + assetDiscoverer.Error = ctx.Err() + return + } + } + }() + + return assetDiscoverer +} + +func convertTags(tags map[string]string) *[]apitypes.Tag { + ret := make([]apitypes.Tag, 0, len(tags)) + for key, val := range tags { + ret = append(ret, apitypes.Tag{ + Key: key, + Value: val, + }) + } + return &ret +} diff --git a/provider/v2/docker/discoverer/image.go b/provider/v2/docker/discoverer/image.go new file mode 100644 index 000000000..3801e3b63 --- /dev/null +++ b/provider/v2/docker/discoverer/image.go @@ -0,0 +1,109 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discoverer + +import ( + "context" + "fmt" + "sync" + + "github.com/docker/docker/api/types" + imagetypes "github.com/docker/docker/api/types/image" + "golang.org/x/sync/errgroup" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/core/log" + "github.com/openclarity/vmclarity/core/to" +) + +func (d *Discoverer) getImageAssets(ctx context.Context) ([]apitypes.AssetType, error) { + logger := log.GetLoggerFromContextOrDiscard(ctx) + + // List all docker images + images, err := d.DockerClient.ImageList(ctx, types.ImageListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list images: %w", err) + } + + // Results will be written to assets concurrently + assetMu := sync.Mutex{} + assets := make([]apitypes.AssetType, 0, len(images)) + + // Process each image in an independent processor goroutine + processGroup, processCtx := errgroup.WithContext(ctx) + for _, image := range images { + processGroup.Go( + // processGroup expects a function with empty signature, so we use a function + // generator to enable adding arguments. This avoids issues when using loop + // variables in goroutines via shared memory space. + // + // If any processor returns an error, it will stop all processors. + // IDEA: Decide what the acceptance criteria should be (e.g. >= 50% images processed) + func(image imagetypes.Summary) func() error { + return func() error { + // Get container image info + info, err := d.getContainerImageInfo(processCtx, image.ID) + if err != nil { + logger.Warnf("Failed to get image. id=%v: %v", image.ID, err) + return nil // skip fail + } + + // Convert to asset + asset := apitypes.AssetType{} + err = asset.FromContainerImageInfo(info) + if err != nil { + return fmt.Errorf("failed to create AssetType from ContainerImageInfo: %w", err) + } + + // Write to assets + assetMu.Lock() + assets = append(assets, asset) + assetMu.Unlock() + + return nil + } + }(image), + ) + } + + // This will block until all the processors have executed successfully or until + // the first error. If an error is returned by any processors, processGroup will + // cancel execution via processCtx and return that error. + err = processGroup.Wait() + if err != nil { + return nil, fmt.Errorf("failed to process images: %w", err) + } + + return assets, nil +} + +func (d *Discoverer) getContainerImageInfo(ctx context.Context, imageID string) (apitypes.ContainerImageInfo, error) { + image, _, err := d.DockerClient.ImageInspectWithRaw(ctx, imageID) + if err != nil { + return apitypes.ContainerImageInfo{}, fmt.Errorf("failed to inspect image: %w", err) + } + + return apitypes.ContainerImageInfo{ + Architecture: to.Ptr(image.Architecture), + ImageID: image.ID, + Labels: convertTags(image.Config.Labels), + RepoTags: to.Ptr(image.RepoTags), + RepoDigests: to.Ptr(image.RepoDigests), + ObjectType: "ContainerImageInfo", + Os: to.Ptr(image.Os), + Size: to.Ptr(image.Size), + }, nil +} diff --git a/provider/v2/docker/estimator/estimator.go b/provider/v2/docker/estimator/estimator.go new file mode 100644 index 000000000..c1cd65771 --- /dev/null +++ b/provider/v2/docker/estimator/estimator.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package estimator + +import ( + "context" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/provider" +) + +var _ provider.Estimator = &Estimator{} + +type Estimator struct{} + +func (e *Estimator) Estimate(ctx context.Context, stats apitypes.AssetScanStats, asset *apitypes.Asset, template *apitypes.AssetScanTemplate) (*apitypes.Estimation, error) { + return &apitypes.Estimation{}, provider.FatalErrorf("Not Implemented") +} diff --git a/provider/v2/docker/provider.go b/provider/v2/docker/provider.go new file mode 100644 index 000000000..437c4392c --- /dev/null +++ b/provider/v2/docker/provider.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker + +import ( + "context" + "fmt" + + "github.com/docker/docker/client" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/v2/docker/discoverer" + "github.com/openclarity/vmclarity/provider/v2/docker/estimator" + "github.com/openclarity/vmclarity/provider/v2/docker/scanner" +) + +var _ provider.Provider = &Provider{} + +type Provider struct { + *discoverer.Discoverer + *scanner.Scanner + *estimator.Estimator +} + +func (p *Provider) Kind() apitypes.CloudProvider { + return apitypes.Docker +} + +func New(_ context.Context) (*Provider, error) { + config, err := NewConfig() + if err != nil { + return nil, fmt.Errorf("invalid configuration. Provider=%s: %w", apitypes.Docker, err) + } + + dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("failed to load provider configuration. Provider=%s: %w", apitypes.Docker, err) + } + + return &Provider{ + Discoverer: &discoverer.Discoverer{ + DockerClient: dockerClient, + }, + Scanner: &scanner.Scanner{ + DockerClient: dockerClient, + HelperImage: config.HelperImage, + NetworkName: config.NetworkName, + }, + Estimator: &estimator.Estimator{}, + }, nil +} diff --git a/provider/v2/docker/scanner/scanner.go b/provider/v2/docker/scanner/scanner.go new file mode 100644 index 000000000..a22f333c2 --- /dev/null +++ b/provider/v2/docker/scanner/scanner.go @@ -0,0 +1,432 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scanner + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/docker/docker/api/types" + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/archive" + "gopkg.in/yaml.v3" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/cli/families" + "github.com/openclarity/vmclarity/core/log" + "github.com/openclarity/vmclarity/provider" +) + +// mountPointPath defines the location in the container where assets will be mounted. +var mountPointPath = "/mnt/snapshot" + +var _ provider.Scanner = &Scanner{} + +type Scanner struct { + DockerClient *client.Client + HelperImage string + NetworkName string +} + +func (s *Scanner) RunAssetScan(ctx context.Context, t *provider.ScanJobConfig) error { + assetVolume, err := s.prepareScanAssetVolume(ctx, t) + if err != nil { + return provider.FatalErrorf("failed to prepare scan volume. Provider=%s: %w", apitypes.Docker, err) + } + + networkID, err := s.createScanNetwork(ctx) + if err != nil { + return provider.FatalErrorf("failed to prepare scan network. Provider=%s: %w", apitypes.Docker, err) + } + + containerID, err := s.createScanContainer(ctx, assetVolume, networkID, t) + if err != nil { + return provider.FatalErrorf("failed to create scan container. Provider=%s: %w", apitypes.Docker, err) + } + + err = s.DockerClient.ContainerStart(ctx, containerID, containertypes.StartOptions{}) + if err != nil { + return provider.FatalErrorf("failed to start scan container. Provider=%s: %w", apitypes.Docker, err) + } + + return nil +} + +func (s *Scanner) RemoveAssetScan(ctx context.Context, t *provider.ScanJobConfig) error { + containerID, err := s.getContainerIDFromName(ctx, t.AssetScanID) + if err != nil { + return provider.FatalErrorf("failed to get scan container id. Provider=%s: %w", apitypes.Docker, err) + } + err = s.DockerClient.ContainerRemove(ctx, containerID, containertypes.RemoveOptions{Force: true}) + if err != nil { + return provider.FatalErrorf("failed to remove scan container. Provider=%s: %w", apitypes.Docker, err) + } + + err = s.DockerClient.VolumeRemove(ctx, t.AssetScanID, true) + if err != nil { + return provider.FatalErrorf("failed to remove volume. Provider=%s: %w", apitypes.Docker, err) + } + + return nil +} + +// prepareScanAssetVolume returns volume name or error. +func (s *Scanner) prepareScanAssetVolume(ctx context.Context, t *provider.ScanJobConfig) (string, error) { + logger := log.GetLoggerFromContextOrDiscard(ctx) + volumeName := t.AssetScanID + + // Create volume if not found + err := s.createScanAssetVolume(ctx, volumeName) + if err != nil { + return "", fmt.Errorf("failed to create scan volume : %w", err) + } + + // Pull image for ephemeral container + imagePullResp, err := s.DockerClient.ImagePull(ctx, s.HelperImage, types.ImagePullOptions{}) + if err != nil { + return "", fmt.Errorf("failed to pull helper image: %w", err) + } + + // Drain response to avoid blocking + _, _ = io.Copy(io.Discard, imagePullResp) + _ = imagePullResp.Close() + + // Create an ephemeral container to populate volume with asset contents + containerResp, err := s.DockerClient.ContainerCreate( + ctx, + &containertypes.Config{ + Image: s.HelperImage, + }, + &containertypes.HostConfig{ + Mounts: []mount.Mount{ + { + Type: mount.TypeVolume, + Source: volumeName, + Target: "/data", + }, + }, + }, + nil, + nil, + "", + ) + if err != nil { + return "", fmt.Errorf("failed to create helper container: %w", err) + } + defer func() { + err := s.DockerClient.ContainerRemove(ctx, containerResp.ID, containertypes.RemoveOptions{Force: true}) + if err != nil { + logger.Errorf("Failed to remove helper container=%s: %v", containerResp.ID, err) + } + }() + + // Export asset data to tar reader + assetContents, exportCleanup, err := s.exportAsset(ctx, t) + if err != nil { + return "", fmt.Errorf("failed to export asset: %w", err) + } + defer func() { + err := assetContents.Close() + if err != nil { + logger.Errorf("failed to close asset contents stream: %s", err.Error()) + } + if exportCleanup != nil { + exportCleanup() + } + }() + + // Copy asset data to ephemeral container + err = s.DockerClient.CopyToContainer(ctx, containerResp.ID, "/data", assetContents, types.CopyToContainerOptions{}) + if err != nil { + return "", fmt.Errorf("failed to copy asset to container: %w", err) + } + + return volumeName, nil +} + +func (s *Scanner) createScanAssetVolume(ctx context.Context, volumeName string) error { + logger := log.GetLoggerFromContextOrDiscard(ctx) + + // Create volume if not found + volumesResp, err := s.DockerClient.VolumeList(ctx, volume.ListOptions{ + Filters: filters.NewArgs(filters.Arg("name", volumeName)), + }) + if err != nil { + return fmt.Errorf("failed to get volumes: %w", err) + } + + if len(volumesResp.Volumes) == 1 { + logger.Infof("Scan volume=%s already exists", volumeName) + return nil + } + if len(volumesResp.Volumes) == 0 { + _, err = s.DockerClient.VolumeCreate(ctx, volume.CreateOptions{ + Name: volumeName, + }) + if err != nil { + return fmt.Errorf("failed to create scan volume: %w", err) + } + return nil + } + return errors.New("invalid number of volumes found") +} + +// createScanNetwork returns network id or error. +func (s *Scanner) createScanNetwork(ctx context.Context) (string, error) { + // Do nothing if network already exists + networkID, _ := s.getNetworkIDFromName(ctx, s.NetworkName) + if networkID != "" { + return networkID, nil + } + + // Create network + networkResp, err := s.DockerClient.NetworkCreate( + ctx, + s.NetworkName, + types.NetworkCreate{ + CheckDuplicate: true, + Driver: "bridge", + }, + ) + if err != nil { + return "", fmt.Errorf("failed to create scan network: %w", err) + } + + return networkResp.ID, nil +} + +// copyScanConfigToContainer copies scan configuration as a file to the scan container. +func (s *Scanner) copyScanConfigToContainer(ctx context.Context, containerID string, t *provider.ScanJobConfig) error { + // Add volume mount point to family configuration + familiesConfig := families.Config{} + err := yaml.Unmarshal([]byte(t.ScannerCLIConfig), &familiesConfig) + if err != nil { + return fmt.Errorf("failed to unmarshal family scan configuration: %w", err) + } + families.SetMountPointsForFamiliesInput([]string{mountPointPath}, &familiesConfig) + familiesConfigByte, err := yaml.Marshal(familiesConfig) + if err != nil { + return fmt.Errorf("failed to marshal family scan configuration: %w", err) + } + + // Write scan config file to temp dir + src := filepath.Join(os.TempDir(), getScanConfigFileName(t)) + err = os.WriteFile(src, familiesConfigByte, 0o400) // nolint:gomnd + if err != nil { + return fmt.Errorf("failed write scan config file: %w", err) + } + + // Create tar archive from scan config file + srcInfo, err := archive.CopyInfoSourcePath(src, false) + if err != nil { + return fmt.Errorf("failed to get copy info: %w", err) + } + srcArchive, err := archive.TarResource(srcInfo) + if err != nil { + return fmt.Errorf("failed to create tar archive: %w", err) + } + defer srcArchive.Close() + + // Prepare archive for copy + dstInfo := archive.CopyInfo{Path: filepath.Join("/", getScanConfigFileName(t))} + dst, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) + if err != nil { + return fmt.Errorf("failed to prepare archive: %w", err) + } + defer preparedArchive.Close() + + // Copy scan config file to container + err = s.DockerClient.CopyToContainer(ctx, containerID, dst, preparedArchive, types.CopyToContainerOptions{}) + if err != nil { + return fmt.Errorf("failed to copy config file to container: %w", err) + } + + return nil +} + +// createScanContainer returns container id or error. +func (s *Scanner) createScanContainer(ctx context.Context, assetVolume, networkID string, t *provider.ScanJobConfig) (string, error) { + containerName := t.AssetScanID + + // Do nothing if scan container already exists + containerID, _ := s.getContainerIDFromName(ctx, containerName) + if containerID != "" { + return containerID, nil + } + + // Pull scanner image if required + images, err := s.DockerClient.ImageList(ctx, types.ImageListOptions{ + Filters: filters.NewArgs(filters.Arg("reference", t.ScannerImage)), + }) + if err != nil { + return "", fmt.Errorf("failed to get images: %w", err) + } + if len(images) == 0 { + imagePullResp, err := s.DockerClient.ImagePull(ctx, t.ScannerImage, types.ImagePullOptions{}) + if err != nil { + return "", fmt.Errorf("failed to pull scanner image: %w", err) + } + // Drain response to avoid blocking + _, _ = io.Copy(io.Discard, imagePullResp) + _ = imagePullResp.Close() + } + + // Create scan container + containerResp, err := s.DockerClient.ContainerCreate( + ctx, + &containertypes.Config{ + Image: t.ScannerImage, + Cmd: []string{ + "scan", + "--config", + filepath.Join("/", getScanConfigFileName(t)), + "--server", + t.VMClarityAddress, + "--asset-scan-id", + t.AssetScanID, + }, + }, + &containertypes.HostConfig{ + Mounts: []mount.Mount{ + { + Type: mount.TypeVolume, + Source: assetVolume, + Target: mountPointPath, + }, + }, + }, + &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + t.AssetScanID: { + NetworkID: networkID, + }, + }, + }, + nil, + containerName, + ) + if err != nil { + return "", fmt.Errorf("failed to create scan container: %w", err) + } + + err = s.copyScanConfigToContainer(ctx, containerResp.ID, t) + if err != nil { + return "", fmt.Errorf("failed to copy scan config to container: %w", err) + } + + return containerResp.ID, nil +} + +func (s *Scanner) getContainerIDFromName(ctx context.Context, containerName string) (string, error) { + containers, err := s.DockerClient.ContainerList(ctx, containertypes.ListOptions{ + All: true, + Filters: filters.NewArgs(filters.Arg("name", containerName)), + }) + if err != nil { + return "", fmt.Errorf("failed to list containers: %w", err) + } + if len(containers) == 0 { + return "", fmt.Errorf("scan container not found: %w", err) + } + if len(containers) > 1 { + return "", fmt.Errorf("found more than one scan container: %w", err) + } + return containers[0].ID, nil +} + +func (s *Scanner) getNetworkIDFromName(ctx context.Context, networkName string) (string, error) { + networks, err := s.DockerClient.NetworkList(ctx, types.NetworkListOptions{ + Filters: filters.NewArgs(filters.Arg("name", networkName)), + }) + if err != nil { + return "", fmt.Errorf("failed to list networks: %w", err) + } + if len(networks) == 0 { + return "", fmt.Errorf("scan network not found: %w", err) + } + if len(networks) > 1 { + for _, n := range networks { + if n.Name == networkName { + return n.ID, nil + } + } + return "", fmt.Errorf("found more than one scan network: %w", err) + } + return networks[0].ID, nil +} + +// nolint:cyclop +func (s *Scanner) exportAsset(ctx context.Context, t *provider.ScanJobConfig) (io.ReadCloser, func(), error) { + logger := log.GetLoggerFromContextOrDiscard(ctx) + + objectType, err := t.AssetInfo.ValueByDiscriminator() + if err != nil { + return nil, nil, fmt.Errorf("failed to get asset object type: %w", err) + } + + switch value := objectType.(type) { + case apitypes.ContainerInfo: + contents, err := s.DockerClient.ContainerExport(ctx, value.ContainerID) + if err != nil { + return nil, nil, fmt.Errorf("failed to export container: %w", err) + } + return contents, nil, nil + + case apitypes.ContainerImageInfo: + // Create an ephemeral container to export asset + containerResp, err := s.DockerClient.ContainerCreate( + ctx, + &containertypes.Config{Image: value.ImageID}, + nil, + nil, + nil, + "", + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to create helper container: %w", err) + } + + cleanup := func() { + err := s.DockerClient.ContainerRemove(ctx, containerResp.ID, containertypes.RemoveOptions{Force: true}) + if err != nil { + logger.Errorf("failed to remove helper container=%s: %v", containerResp.ID, err) + } + } + + contents, err := s.DockerClient.ContainerExport(ctx, containerResp.ID) + if err != nil { + cleanup() + return nil, nil, fmt.Errorf("failed to export container: %w", err) + } + return contents, cleanup, nil + + default: + return nil, nil, fmt.Errorf("failed to export asset object type %T: Not implemented", value) + } +} + +func getScanConfigFileName(t *provider.ScanJobConfig) string { + return fmt.Sprintf("scanconfig_%s.yaml", t.AssetScanID) +}