@@ -17,6 +17,7 @@ package cli
1717import (
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.
8094func 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+
342501type pkginfo struct {
343502 pkgname string
344503 pkgver string
0 commit comments