mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 00:06:58 +00:00
181 lines
5.4 KiB
Go
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
|
|
}
|