Files
at-container-registry/scanner/internal/scan/extractor.go
T

181 lines
5.4 KiB
Go

package scan
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
scanner "atcr.io/scanner"
"atcr.io/scanner/internal/client"
)
// extractLayers downloads and extracts all image layers via presigned URLs
// Returns the rootfs directory path and a cleanup function
func extractLayers(job *scanner.ScanJob, tmpDir string) (string, func(), error) {
scanDir, err := os.MkdirTemp(tmpDir, "scan-*")
if err != nil {
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
}
cleanup := func() {
if err := os.RemoveAll(scanDir); err != nil {
slog.Warn("Failed to clean up temp directory", "dir", scanDir, "error", err)
}
}
imageDir := filepath.Join(scanDir, "image")
rootfsDir := filepath.Join(imageDir, "rootfs")
layersDir := filepath.Join(imageDir, "layers")
for _, dir := range []string{rootfsDir, layersDir} {
if err := os.MkdirAll(dir, 0755); err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
// Download and validate config blob
slog.Info("Downloading config blob", "digest", job.Config.Digest)
configPath := filepath.Join(imageDir, "config.json")
if err := downloadBlobViaPresignedURL(job.HoldEndpoint, job.HoldDID, job.Config.Digest, configPath); err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to download config blob: %w", err)
}
configData, err := os.ReadFile(configPath)
if err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to read config: %w", err)
}
var configObj map[string]interface{}
if err := json.Unmarshal(configData, &configObj); err != nil {
cleanup()
return "", nil, fmt.Errorf("invalid config JSON: %w", err)
}
// Download and extract each layer
for i, layer := range job.Layers {
slog.Info("Extracting layer", "index", i, "digest", layer.Digest, "size", layer.Size)
layerPath := filepath.Join(layersDir, fmt.Sprintf("layer-%d.tar.gz", i))
if err := downloadBlobViaPresignedURL(job.HoldEndpoint, job.HoldDID, layer.Digest, layerPath); err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to download layer %d: %w", i, err)
}
if err := extractTarGz(layerPath, rootfsDir); err != nil {
cleanup()
return "", nil, fmt.Errorf("failed to extract layer %d: %w", i, err)
}
// Remove layer tar.gz to save space
os.Remove(layerPath)
}
entries, err := os.ReadDir(rootfsDir)
if err != nil {
slog.Warn("Failed to read rootfs directory", "error", err)
} else {
slog.Info("Successfully extracted image",
"layers", len(job.Layers),
"topLevelEntries", len(entries))
}
return rootfsDir, cleanup, nil
}
// downloadBlobViaPresignedURL gets a presigned URL from the hold and downloads the blob
func downloadBlobViaPresignedURL(holdEndpoint, holdDID, digest, destPath string) error {
presignedURL, err := client.GetBlobPresignedURL(holdEndpoint, holdDID, digest)
if err != nil {
return fmt.Errorf("failed to get presigned URL for %s: %w", digest, err)
}
return client.DownloadBlob(presignedURL, destPath)
}
// extractTarGz extracts a tar.gz file to a destination directory (overlayfs style)
func extractTarGz(tarGzPath, destDir string) error {
file, err := os.Open(tarGzPath)
if err != nil {
return fmt.Errorf("failed to open tar.gz: %w", err)
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("failed to read tar header: %w", err)
}
target := filepath.Join(destDir, filepath.Clean(header.Name))
// Security: ensure target is within destDir
if !strings.HasPrefix(target, filepath.Clean(destDir)+string(os.PathSeparator)) {
slog.Warn("Skipping path outside destination", "path", header.Name)
continue
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("failed to create directory %s: %w", target, err)
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return fmt.Errorf("failed to create parent directory: %w", err)
}
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return fmt.Errorf("failed to create file %s: %w", target, err)
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return fmt.Errorf("failed to write file %s: %w", target, err)
}
outFile.Close()
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return fmt.Errorf("failed to create parent directory for symlink: %w", err)
}
os.Remove(target)
if err := os.Symlink(header.Linkname, target); err != nil {
slog.Warn("Failed to create symlink", "target", target, "link", header.Linkname, "error", err)
}
case tar.TypeLink:
linkTarget := filepath.Join(destDir, filepath.Clean(header.Linkname))
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return fmt.Errorf("failed to create parent directory for hardlink: %w", err)
}
os.Remove(target)
if err := os.Link(linkTarget, target); err != nil {
slog.Warn("Failed to create hardlink", "target", target, "link", linkTarget, "error", err)
}
default:
slog.Debug("Skipping unsupported tar entry type", "type", header.Typeflag, "name", header.Name)
}
}
return nil
}