Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions internal/catalog/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,16 @@ func catalogByGlob(resolver file.Resolver, root, glob, label string, parse fileP
i, loc := i, loc
g.Go(func() error {
pkgs, err := parse(filepath.Join(root, loc.RealPath), loc)
if err != nil || len(pkgs) == 0 {
return nil // per-file errors are non-fatal, as before
if err != nil {
// Non-fatal, but surface it: a swallowed uv/npm failure (e.g.
// the host Python is too old to resolve the pins) otherwise
// looks identical to "nothing found". Warn to stderr — the SBOM
// goes to stdout, so this never corrupts --local output.
fmt.Fprintf(os.Stderr, "ossprey: %s cataloger: %v\n", label, err)
return nil
}
if len(pkgs) == 0 {
return nil
}
mu.Lock()
results = append(results, result{idx: i, pkgs: pkgs})
Expand Down
47 changes: 47 additions & 0 deletions internal/catalog/requirements_cataloger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package catalog

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)

// RequirementsCataloger resolves transitive Python deps from a requirements.txt
// by invoking `uv pip compile`. Syft's built-in cataloger reads the file
// literally (direct deps only); this fills in the transitive closure, pinning
// every dependency to a concrete version resolved against PyPI.
type RequirementsCataloger struct {
root string
}

func NewRequirementsCataloger(root string) *RequirementsCataloger {
return &RequirementsCataloger{root: root}
}

func (c *RequirementsCataloger) Name() string { return "ossprey-requirements-cataloger" }

func (c *RequirementsCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
uv, err := exec.LookPath("uv")
if err != nil {
return nil, nil, nil // no uv on PATH — silently skip
}
cache, err := os.MkdirTemp("", "ossprey-uv-cache-")
if err != nil {
return nil, nil, fmt.Errorf("uv cache: %w", err)
}
defer os.RemoveAll(cache)

parse := func(absPath string, loc file.Location) ([]pkg.Package, error) {
dir := filepath.Dir(absPath)
args := []string{"pip", "compile", "--universal", "--no-progress", absPath}
return runUV(ctx, uv, cache, dir, args, loc)
}
out, err := catalogByGlob(resolver, c.root, "**/requirements.txt", "requirements", parse)
return out, nil, err
}
4 changes: 4 additions & 0 deletions internal/catalog/syft.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func Catalog(ctx context.Context, path string) ([]Package, error) {
// Custom: resolve transitives from setup.py when pyproject is absent
// or lacks a [project] table (legacy setuptools projects).
NewSetupPyCataloger(absRoot),
// Custom: resolve transitives from requirements.txt via uv (syft's
// built-in reads the file literally — direct deps only).
NewRequirementsCataloger(absRoot),
// Custom: direct-deps fallback for pyproject.toml when uv is missing.
NewPyProjectCataloger(absRoot),
// Custom: resolve npm ranges to concrete versions via `npm install
Expand Down Expand Up @@ -188,6 +191,7 @@ func isOspreyCataloger(name string) bool {
switch name {
case "ossprey-uv-cataloger",
"ossprey-setuppy-cataloger",
"ossprey-requirements-cataloger",
"ossprey-pyproject-cataloger",
"ossprey-npm-cataloger",
"ossprey-packagejson-cataloger":
Expand Down
Loading