mirror of
https://github.com/anchore/syft.git
synced 2026-11-04 08:26:38 +01:00
* ci: run the full unit suite on windows via make unit Drops the curated `unit:windows` subset; the windows job now bootstraps like linux and runs `make unit`. Exceptions get added as failures show up. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * ci: skip fixture refresh for windows unit tests binny has no windows release, so the bootstrap action can't install the tools the fixture refresh needs. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test: skip linux container fixtures when docker can't run them Windows CI runners only run windows containers, so image fixtures go through `testutils.SkipWithoutLinuxContainers`. No behavior change on linux/mac. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: close leaked file handles and posix squashfs paths on windows - output writers are now closed on early returns in `scan`/`convert` (open handles block cleanup on windows) - `newSBOMMultiWriter` used `path.Dir` on host paths, so output subdirectories were never created on windows - squashfs walk paths were built with `filepath.Join` - skip a few posix-only test cases (dir perms, xdg fallback, go-rpmdb ndb handle leak upstream) Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(golang): resolve go source module dir natively on windows - `parseGoModFile` passed the resolver's posix (`/c/...`) path to the go toolchain, so go source resolution silently found nothing on windows - skip container-built fixtures (go archs, java builds) and golden image tarballs on windows; stereoscope's layer cache filenames contain `:`, which windows rejects Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: keep posix paths posix on windows - `ChrootContext.ToChrootPath` nested volumes (`D:\cwd\C:\root`) when the scan root was on another drive, so the prefix was never trimmed - bitnami, python wheel/egg, and alpm parsers used `filepath` on posix location paths - alpm backup-file lookups silently missed on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * ci: pull the test fixture cache for windows unit tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: don't map virtual file tree paths through the host on windows `FiletreeResolver` without a chroot (snaps) ran request paths through `filepath.Abs`, rooting them on the current volume (`/payload.txt` -> `/d/payload.txt`), so snap scans on windows found nothing. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: match tar entry globs as posix on windows - `matchesAnyGlob` used `doublestar.PathMatch` (host separator) on tar entry names, so tar-wrapped java archives silently produced nothing on windows - test helpers now treat binary snippet keys and `FromFile` locations as posix Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * incorporate windows path fix Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * incorporate go-rpmdb close fix Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * close squashfs fixture writers before finalizing - windows dir listings can report a zero size for files still open, so the manifest was packed empty (also adds a temporary windows diagnostic for the binary cataloger) Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * temp: surface windows binary cataloger diagnostics Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: keep directory symlink targets posix on windows - the directory indexer joined symlink targets with native separators, so links in the posix file tree pointed nowhere and globs on link names found nothing - binary fixture logical keys used native separators, so nested snippets were picked as the fixture dir on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep addSymlinkToIndex under the funlen limit Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * drop windows skips covered by the stereoscope and go-rpmdb bumps Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
181 lines
5.0 KiB
Go
181 lines
5.0 KiB
Go
/*
|
|
Package bitnami provides a concrete Cataloger implementation for capturing packages embedded within Bitnami SBOM files.
|
|
*/
|
|
package bitnami
|
|
|
|
import (
|
|
"context"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/anchore/syft/internal/log"
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/format"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
|
)
|
|
|
|
const catalogerName = "bitnami-cataloger"
|
|
|
|
// NewCataloger returns a new SBOM cataloger object loaded from saved SBOM JSON.
|
|
func NewCataloger() pkg.Cataloger {
|
|
return generic.NewCataloger(catalogerName).
|
|
WithParserByGlobs(parseSBOM,
|
|
"/opt/bitnami/**/.spdx-*.spdx",
|
|
).
|
|
WithParserByGlobs(parseComponentsJSON,
|
|
"/opt/bitnami/.bitnami_components.json",
|
|
)
|
|
}
|
|
|
|
func parseSBOM(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
|
s, sFormat, _, err := format.Decode(reader)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if s == nil {
|
|
log.WithFields("path", reader.RealPath).Trace("file is not an SBOM")
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// Bitnami exclusively uses SPDX JSON SBOMs
|
|
if sFormat != "spdx-json" {
|
|
log.WithFields("path", reader.RealPath).Trace("file is not an SPDX JSON SBOM")
|
|
return nil, nil, nil
|
|
}
|
|
|
|
var pkgs []pkg.Package
|
|
var secondaryPkgsFiles []string
|
|
mainPkgID := findMainPkgID(s.Relationships)
|
|
for _, p := range s.Artifacts.Packages.Sorted() {
|
|
// We only want to report Bitnami packages
|
|
if !strings.HasPrefix(p.PURL, "pkg:bitnami") {
|
|
continue
|
|
}
|
|
|
|
p.FoundBy = catalogerName
|
|
p.Type = pkg.BitnamiPkg
|
|
// replace all locations on the package with the location of the SBOM file.
|
|
// Why not keep the original list of locations? Since the "locations" field is meant to capture
|
|
// where there is evidence of this file, and the catalogers have not run against any file other than,
|
|
// the SBOM, this is the only location that is relevant for this cataloger.
|
|
p.Locations = file.NewLocationSet(
|
|
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
|
)
|
|
|
|
// Parse the Bitnami-specific metadata
|
|
metadata, err := parseBitnamiPURL(p.PURL)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Bitnami packages reported in a SPDX file are shipped under the same directory
|
|
// as the SPDX file itself.
|
|
metadata.Path = path.Dir(reader.RealPath)
|
|
if p.ID() != mainPkgID {
|
|
metadata.Files = packageFiles(s.Relationships, p, metadata.Path)
|
|
secondaryPkgsFiles = append(secondaryPkgsFiles, metadata.Files...)
|
|
}
|
|
|
|
p.Metadata = metadata
|
|
|
|
pkgs = append(pkgs, p)
|
|
}
|
|
// If there is exactly one package, assume it is the main package
|
|
if len(pkgs) == 1 && mainPkgID == "" {
|
|
mainPkgID = pkgs[0].ID()
|
|
}
|
|
|
|
// Resolve all files owned by the main package in the SBOM and update the metadata
|
|
if mainPkgFiles, err := mainPkgFiles(resolver, reader.RealPath, secondaryPkgsFiles); err == nil {
|
|
for i, p := range pkgs {
|
|
if p.ID() == mainPkgID {
|
|
metadata, ok := p.Metadata.(*pkg.BitnamiSBOMEntry)
|
|
if !ok {
|
|
log.WithFields("spdx-filepath", reader.RealPath).Trace("main package in SBOM does not have Bitnami metadata")
|
|
continue
|
|
}
|
|
|
|
metadata.Files = mainPkgFiles
|
|
pkgs[i].Metadata = metadata
|
|
}
|
|
}
|
|
} else {
|
|
log.WithFields("spdx-filepath", reader.RealPath, "error", err).Trace("unable to resolve owned files for main package in SBOM")
|
|
}
|
|
|
|
return pkgs, filterRelationships(s.Relationships, pkgs), nil
|
|
}
|
|
|
|
// filterRelationships filters out relationships that are not related to Bitnami packages
|
|
// and replaces the package information with the one with completed info
|
|
func filterRelationships(relationships []artifact.Relationship, pkgs []pkg.Package) []artifact.Relationship {
|
|
var result []artifact.Relationship
|
|
for _, r := range relationships {
|
|
if value, ok := r.From.(pkg.Package); ok {
|
|
found := false
|
|
for _, p := range pkgs {
|
|
if value.PURL == p.PURL {
|
|
r.From = p
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
if value, ok := r.To.(pkg.Package); ok {
|
|
found := false
|
|
for _, p := range pkgs {
|
|
if value.PURL == p.PURL {
|
|
r.To = p
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
result = append(result, r)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// findMainPkgID goes through the list of relationships and finds the main package ID
|
|
// which is the one that contains other packages but is not contained by any other package
|
|
func findMainPkgID(relationships []artifact.Relationship) artifact.ID {
|
|
containedByAnother := func(candidateID artifact.ID) bool {
|
|
for _, r := range relationships {
|
|
if r.Type != artifact.ContainsRelationship {
|
|
continue
|
|
}
|
|
|
|
if to, ok := r.To.(pkg.Package); ok {
|
|
if to.ID() == candidateID {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
for _, r := range relationships {
|
|
if from, ok := r.From.(pkg.Package); ok {
|
|
if !strings.HasPrefix(from.PURL, "pkg:bitnami") {
|
|
continue
|
|
}
|
|
if !containedByAnother(from.ID()) {
|
|
return from.ID()
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|