Files
syft/syft/pkg/cataloger/nix/db_cataloger_v10.go
T
Will Murphy e38851143e chore: centralize temp files and prefer streaming IO (#4668)
* chore: centralize temp files and prefer streaming IO

Catalogers that create temp files ad-hoc can easily forget cleanup,
leaking files on disk. Similarly, io.ReadAll is convenient but risks
OOM on large or malicious inputs.

Introduce internal/tmpdir to manage all cataloger temp storage under
a single root directory with automatic cleanup. Prefer streaming
parsers (bufio.Scanner, json/yaml.NewDecoder, io.LimitReader) over
buffering entire inputs into memory. Add ruleguard rules to enforce
both practices going forward.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* chore: go back to old release parsing

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* simplify to limit reader in version check

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* chore: regex change postponed

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* simplify supplement release to limitreader

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

---------

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-03-18 10:53:51 -04:00

261 lines
6.6 KiB
Go

package nix
import (
"context"
"database/sql"
"fmt"
"io"
"os"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
var _ dbProcessor = processV10DB
func processV10DB(ctx context.Context, config Config, dbLocation file.Location, resolver file.Resolver, catalogerName string) ([]pkg.Package, []artifact.Relationship, error) {
dbContents, err := resolver.FileContentsByLocation(dbLocation)
defer internal.CloseAndLogError(dbContents, dbLocation.RealPath)
if err != nil {
return nil, nil, fmt.Errorf("unable to read Nix database: %w", err)
}
td := tmpdir.FromContext(ctx)
if td == nil {
return nil, nil, fmt.Errorf("no temp dir factory in context")
}
tempDB, cleanupDB, err := createTempDB(td, dbContents)
if err != nil {
return nil, nil, fmt.Errorf("failed to create temporary database: %w", err)
}
// defer order is LIFO: cleanupDB (remove file) must run after db.Close and tempDB.Close
defer cleanupDB()
// close order is LIFO: db.Close() (SQLite conn) → tempDB.Close() (file handle) → cleanupDB (remove file)
defer tempDB.Close()
db, err := sql.Open("sqlite", tempDB.Name())
if err != nil {
return nil, nil, fmt.Errorf("failed to open database: %w", err)
}
defer db.Close()
db.SetConnMaxLifetime(0)
packageEntries, err := extractV10DBPackages(config, db, dbLocation, resolver)
if err != nil {
return nil, nil, err
}
pkgs, relationships, err := finalizeV10DBResults(db, packageEntries, catalogerName)
if err != nil {
return nil, nil, err
}
return pkgs, relationships, nil
}
func extractV10DBPackages(config Config, db *sql.DB, dbLocation file.Location, resolver file.Resolver) (map[int]*dbPackageEntry, error) {
pkgs, err := extractV10DBValidPaths(config, db, dbLocation, resolver)
if err != nil {
return nil, err
}
err = extractV10DBDerivationOutputs(db, pkgs)
if err != nil {
return nil, err
}
return pkgs, nil
}
func extractV10DBValidPaths(config Config, db *sql.DB, dbLocation file.Location, resolver file.Resolver) (map[int]*dbPackageEntry, error) {
packages := make(map[int]*dbPackageEntry)
rows, err := db.Query("SELECT id, path, hash, deriver FROM ValidPaths")
if err != nil {
return nil, fmt.Errorf("failed to query ValidPaths: %w", err)
}
defer rows.Close()
for rows.Next() {
var id int
var path, hash, deriver sql.NullString
if err := rows.Scan(&id, &path, &hash, &deriver); err != nil {
return nil, fmt.Errorf("failed to scan ValidPaths row: %w", err)
}
if !path.Valid {
continue
}
nsp := parseNixStorePath(path.String)
if nsp == nil {
nsp = &nixStorePath{}
}
// always trust the DB values over string parsing
nsp.OutputHash = hash.String
nsp.StorePath = path.String
var files []string
if config.CaptureOwnedFiles {
files = listOutputPaths(path.String, resolver)
}
df, err := newDerivationFromPath(deriver.String, resolver)
if err != nil {
log.WithFields("path", deriver.String, "error", err).Trace("unable to find derivation")
df = nil
}
packages[id] = &dbPackageEntry{
ID: id,
nixStorePath: *nsp,
derivationFile: df,
DeriverPath: deriver.String,
Location: &dbLocation,
Files: files,
}
}
return packages, nil
}
func listOutputPaths(storePath string, resolver file.Resolver) []string {
if storePath == "" {
return nil
}
searchGlob := storePath + "/**/*"
locations, err := resolver.FilesByGlob(searchGlob)
if err != nil {
log.WithFields("path", storePath, "error", err).Trace("unable to find output paths")
return nil
}
return filePaths(locations)
}
func extractV10DBDerivationOutputs(db *sql.DB, packages map[int]*dbPackageEntry) error {
outputRows, err := db.Query("SELECT drv, id, path FROM DerivationOutputs")
if err != nil {
return fmt.Errorf("failed to query DerivationOutputs: %w", err)
}
defer outputRows.Close()
pkgsByPath := make(map[string]*dbPackageEntry)
for _, p := range packages {
pkgsByPath[p.StorePath] = p
}
for outputRows.Next() {
var drvID int
var outputID, outputPath string
if err := outputRows.Scan(&drvID, &outputID, &outputPath); err != nil {
return fmt.Errorf("failed to scan DerivationOutputs row: %w", err)
}
if _, ok := pkgsByPath[outputPath]; !ok {
continue
}
pkgsByPath[outputPath].Output = outputID
pkgsByPath[outputPath].DrvID = drvID
}
return nil
}
func finalizeV10DBResults(db *sql.DB, packageEntries map[int]*dbPackageEntry, catalogerName string) ([]pkg.Package, []artifact.Relationship, error) {
// make Syft packages for each package entry
syftPackages := make(map[int]pkg.Package)
for id, entry := range packageEntries {
syftPackages[id] = newDBPackage(entry, catalogerName)
}
var relationships []artifact.Relationship
query := `
SELECT r.referrer, r.reference
FROM Refs r
JOIN ValidPaths v1 ON r.referrer = v1.id
JOIN ValidPaths v2 ON r.reference = v2.id
`
refRows, err := db.Query(query)
if err != nil {
return nil, nil, fmt.Errorf("failed to query Refs with ValidPaths JOIN: %w", err)
}
defer refRows.Close()
relExists := make(map[int]map[int]bool)
for refRows.Next() {
var referrerID, referenceID int
if err := refRows.Scan(&referrerID, &referenceID); err != nil {
return nil, nil, fmt.Errorf("failed to scan Refs row: %w", err)
}
if referrerID == referenceID {
// skip self-references
continue
}
referrer, refExists := syftPackages[referrerID]
reference, refeeExists := syftPackages[referenceID]
if !refExists || !refeeExists {
// only include relationships for packages we have discovered
continue
}
if _, ok := relExists[referrerID]; !ok {
relExists[referrerID] = make(map[int]bool)
}
if relExists[referrerID][referenceID] {
// deduplicate existing relationships
continue
}
relExists[referrerID][referenceID] = true
rel := artifact.Relationship{
From: reference,
To: referrer,
Type: artifact.DependencyOfRelationship,
}
relationships = append(relationships, rel)
}
var pkgs []pkg.Package
for _, p := range syftPackages {
pkgs = append(pkgs, p)
}
return pkgs, relationships, nil
}
func createTempDB(td *tmpdir.TempDir, content io.ReadCloser) (*os.File, func(), error) {
noop := func() {}
tempFile, cleanup, err := td.NewFile("nix-db-*.sqlite") //nolint:gocritic // cleanup is returned to caller, not deferred here
if err != nil {
return nil, noop, err
}
_, err = io.Copy(tempFile, content)
if err != nil {
tempFile.Close()
cleanup()
return nil, noop, err
}
return tempFile, cleanup, nil
}