diff --git a/main.go b/main.go index 6e440c07..b312c21a 100644 --- a/main.go +++ b/main.go @@ -364,7 +364,8 @@ func findAllLockfiles(r *reporter.Reporter, pathsToCheck []string, parseAs strin return false } - if strings.HasSuffix(string(path), "/node_modules") { + if strings.HasSuffix(string(path), "/node_modules") || + strings.HasSuffix(string(path), "/site-packages") { paths = append(paths, string(path)) return false @@ -422,6 +423,16 @@ func parseLockfile(pathToLock string, args []string, img *image.Image) (lockfile return l, err } + if isPathTo(pathToLock, "site-packages") { + l, err := lockfile.WalkPythonSitePackagesInImage(*img, pathToLock) + + if err != nil { + err = fmt.Errorf("%w", err) + } + + return l, err + } + if pathToLock == "/lib/apk/db/installed" { r, err := img.OpenPathFromSquash(file.Path(pathToLock)) if err != nil { diff --git a/pkg/lockfile/walk-python-site-packages.go b/pkg/lockfile/walk-python-site-packages.go index ce82cf73..fbd24040 100644 --- a/pkg/lockfile/walk-python-site-packages.go +++ b/pkg/lockfile/walk-python-site-packages.go @@ -3,6 +3,10 @@ package lockfile import ( "bufio" "fmt" + "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/filetree" + "github.com/anchore/stereoscope/pkg/filetree/filenode" + "github.com/anchore/stereoscope/pkg/image" "io" "io/fs" "os" @@ -49,6 +53,88 @@ func readPythonSitePackageMetadata(r io.Reader) (name, version string, err error return name, version, err } +func WalkPythonSitePackagesInImage(img image.Image, pathToPythonSitePackages string) (Lockfile, error) { + var packages []PackageDetails + + err := img.SquashedTree().Walk( + func(path file.Path, f filenode.FileNode) error { + metadataFile := "" + + if strings.HasSuffix(string(path), ".dist-info") { + metadataFile = "METADATA" + } + + if strings.HasSuffix(string(path), ".egg-info") { + metadataFile = "PKG-INFO" + } + + if metadataFile != "" { + r, err := img.OpenPathFromSquash(path + file.Path("/"+metadataFile)) + + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + + return nil + } + + name, version, err := readPythonSitePackageMetadata(r) + + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + + return nil + } + + packages = append(packages, PackageDetails{ + Name: name, + Version: version, + Commit: "", + Ecosystem: PipEcosystem, + CompareAs: PipEcosystem, + }) + + return nil + } + + return nil + }, + &filetree.WalkConditions{ + ShouldVisit: func(path file.Path, node filenode.FileNode) bool { + // we only want to visit the node if: + // 1. it is a directory + // 2. it is within the given site-packages directory + return node.FileType == file.TypeDirectory && + strings.HasPrefix(string(path), pathToPythonSitePackages) + }, + ShouldContinueBranch: func(path file.Path, node filenode.FileNode) bool { + // We want to avoid any symlinks as they could be cyclical, and they should + // be safe to skip since we should end up walking their targets eventually + if IsSymlink(path, node) { + return false + } + + // we don't actually need to explore these directories, as site-packages is a flat module tree, + // and we can access the metadata file within the directory when we visit it + return !strings.HasSuffix(string(path), ".dist-info") && !strings.HasSuffix(string(path), ".egg-info") + }, + }, + ) + + sort.Slice(packages, func(i, j int) bool { + if packages[i].Name == packages[j].Name { + return packages[i].Version < packages[j].Version + } + + return packages[i].Name < packages[j].Name + }) + + return Lockfile{ + FilePath: pathToPythonSitePackages, + ParsedAs: "site-packages", + Packages: packages, + }, err +} + func WalkPythonSitePackages(pathToPythonSitePackages string) (Lockfile, error) { var packages []PackageDetails