Skip to content

Commit b9f6cbd

Browse files
committed
new command: scan-tar
1 parent 6c880f7 commit b9f6cbd

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/in-toto/attestation v1.1.2
2121
github.com/invopop/jsonschema v0.13.0
2222
github.com/joho/godotenv v1.5.1
23+
github.com/jonjohnsonjr/targz v0.0.0-20241113200849-4986e08f3fb4
2324
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
2425
github.com/klauspost/compress v1.18.0
2526
github.com/klauspost/pgzip v1.2.6

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
216216
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
217217
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
218218
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
219+
github.com/jonjohnsonjr/targz v0.0.0-20241113200849-4986e08f3fb4 h1:yzUKZR6eq4hfKkNLe2KfxOBiVHyjXny7g4bEDuiYCtY=
220+
github.com/jonjohnsonjr/targz v0.0.0-20241113200849-4986e08f3fb4/go.mod h1:vFsMbFCBsTclpEtIkbCOBAJj1mBsqoMtm22ibo1cG2o=
219221
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
220222
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
221223
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=

pkg/cli/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func New() *cobra.Command {
6262
cmd.AddCommand(packageVersion())
6363
cmd.AddCommand(query())
6464
cmd.AddCommand(scan())
65+
cmd.AddCommand(scanTar())
6566
cmd.AddCommand(signCmd())
6667
cmd.AddCommand(signIndex())
6768
cmd.AddCommand(test())

pkg/cli/scan.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package cli
1717
import (
1818
"bufio"
1919
"bytes"
20+
"compress/gzip"
2021
"context"
2122
"fmt"
2223
"io"
@@ -34,6 +35,7 @@ import (
3435
"chainguard.dev/melange/pkg/config"
3536
"chainguard.dev/melange/pkg/sca"
3637
"github.com/chainguard-dev/clog"
38+
"github.com/jonjohnsonjr/targz/tarfs"
3739
"github.com/spf13/cobra"
3840
"go.opentelemetry.io/otel"
3941
)
@@ -76,6 +78,18 @@ func scan() *cobra.Command {
7678
return cmd
7779
}
7880

81+
func scanTar() *cobra.Command {
82+
return &cobra.Command{
83+
Use: "scan-tar",
84+
Short: "Scan a tar stream from stdin and analyze dependencies",
85+
Example: `docker export container_id | melange scan-tar --name mypackage --version 1.0.0`,
86+
Args: cobra.NoArgs,
87+
RunE: func(cmd *cobra.Command, args []string) error {
88+
return scanTarCmd(cmd.Context())
89+
},
90+
}
91+
}
92+
7993
// TODO: It would be cool if there was a way this could take just a directory.
8094
func scanCmd(ctx context.Context, file string, sc *scanConfig) error {
8195
ctx, span := otel.Tracer("melange").Start(ctx, "scan")
@@ -339,6 +353,151 @@ func scanCmd(ctx context.Context, file string, sc *scanConfig) error {
339353
return nil
340354
}
341355

356+
// scanTarCmd processes a tar stream from stdin and analyzes it for dependencies
357+
func scanTarCmd(ctx context.Context) error {
358+
ctx, span := otel.Tracer("melange").Start(ctx, "scan-tar")
359+
defer span.End()
360+
361+
log := clog.FromContext(ctx)
362+
363+
// Create temporary file to store the tar stream
364+
// This is necessary because apko's tarfs requires io.ReaderAt (random access)
365+
tmpFile, err := os.CreateTemp("", "melange-scan-*.tar")
366+
if err != nil {
367+
return fmt.Errorf("create temp file: %w", err)
368+
}
369+
defer os.Remove(tmpFile.Name())
370+
defer tmpFile.Close()
371+
372+
log.Infof("Reading tar stream from stdin...")
373+
374+
// Handle potential gzip compression
375+
var reader io.Reader = os.Stdin
376+
377+
// Try to detect gzip magic bytes
378+
peekReader := bufio.NewReader(os.Stdin)
379+
peek, err := peekReader.Peek(2)
380+
if err != nil && err != io.EOF {
381+
return fmt.Errorf("peek stdin: %w", err)
382+
}
383+
384+
// Check for gzip magic bytes (1f 8b)
385+
if len(peek) >= 2 && peek[0] == 0x1f && peek[1] == 0x8b {
386+
log.Infof("Detected gzip-compressed tar stream")
387+
gzReader, err := gzip.NewReader(peekReader)
388+
if err != nil {
389+
return fmt.Errorf("create gzip reader: %w", err)
390+
}
391+
defer gzReader.Close()
392+
reader = gzReader
393+
} else {
394+
reader = peekReader
395+
}
396+
397+
// Copy the tar stream to temporary file
398+
written, err := io.Copy(tmpFile, reader)
399+
if err != nil {
400+
return fmt.Errorf("copy tar stream: %w", err)
401+
}
402+
403+
log.Infof("Wrote %d bytes to temporary file", written)
404+
405+
if written == 0 {
406+
return fmt.Errorf("no data received from stdin")
407+
}
408+
409+
// Seek back to beginning for reading
410+
if _, err := tmpFile.Seek(0, 0); err != nil {
411+
return fmt.Errorf("seek temp file: %w", err)
412+
}
413+
414+
// Create our custom TarSCAHandle
415+
tarHandle, err := newTarSCAHandle(tmpFile)
416+
if err != nil {
417+
return fmt.Errorf("create tar SCA handle: %w", err)
418+
}
419+
420+
// Run SCA analysis
421+
generated := &config.Dependencies{}
422+
if err := sca.Analyze(ctx, tarHandle, generated); err != nil {
423+
return fmt.Errorf("SCA analysis: %w", err)
424+
}
425+
426+
// For tar scanning, remove versions from command provides since commands
427+
// don't have meaningful separate versions from the container
428+
for i, provide := range generated.Provides {
429+
if strings.HasPrefix(provide, "cmd:") {
430+
if idx := strings.Index(provide, "="); idx != -1 {
431+
generated.Provides[i] = provide[:idx]
432+
}
433+
}
434+
}
435+
436+
// Output results in the same format as regular scan
437+
log.Infof("Analysis complete. Found %d runtime deps, %d provides, %d vendored",
438+
len(generated.Runtime), len(generated.Provides), len(generated.Vendored))
439+
440+
// Create a minimal PackageBuild for output formatting
441+
pkg := &config.Package{}
442+
443+
bb := &build.Build{
444+
Configuration: &config.Configuration{
445+
Package: *pkg,
446+
},
447+
}
448+
449+
pb := &build.PackageBuild{
450+
Build: bb,
451+
Origin: pkg,
452+
Dependencies: *generated,
453+
}
454+
455+
var buf bytes.Buffer
456+
if err := pb.GenerateControlData(&buf); err != nil {
457+
return fmt.Errorf("generate control data: %w", err)
458+
}
459+
460+
os.Stdout.Write(buf.Bytes())
461+
return nil
462+
}
463+
464+
// TarSCAHandle implements sca.SCAHandle for tar files
465+
type TarSCAHandle struct {
466+
tarFile *os.File
467+
tarFS *tarfs.FS
468+
}
469+
470+
// newTarSCAHandle creates a new TarSCAHandle from a tar file
471+
func newTarSCAHandle(tarFile *os.File) (*TarSCAHandle, error) {
472+
// Get file info to determine size
473+
stat, err := tarFile.Stat()
474+
if err != nil {
475+
return nil, fmt.Errorf("stat tar file: %w", err)
476+
}
477+
478+
// Create tarfs filesystem
479+
fs, err := tarfs.New(tarFile, stat.Size())
480+
if err != nil {
481+
return nil, fmt.Errorf("create tar filesystem: %w", err)
482+
}
483+
484+
return &TarSCAHandle{
485+
tarFile: tarFile,
486+
tarFS: fs,
487+
}, nil
488+
}
489+
490+
func (t *TarSCAHandle) PackageName() string { return "" }
491+
func (t *TarSCAHandle) RelativeNames() []string { return []string{} }
492+
func (t *TarSCAHandle) Version() string { return "" }
493+
func (t *TarSCAHandle) FilesystemForRelative(pkgName string) (sca.SCAFS, error) { return t.tarFS, nil }
494+
func (t *TarSCAHandle) Filesystem() (sca.SCAFS, error) { return t.tarFS, nil }
495+
func (t *TarSCAHandle) Options() config.PackageOption { return config.PackageOption{} }
496+
func (t *TarSCAHandle) BaseDependencies() config.Dependencies { return config.Dependencies{} }
497+
func (t *TarSCAHandle) InstalledPackages() map[string]string { return map[string]string{} }
498+
func (t *TarSCAHandle) PkgResolver() *apk.PkgResolver { return nil }
499+
500+
342501
type pkginfo struct {
343502
pkgname string
344503
pkgver string

0 commit comments

Comments
 (0)