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>
This commit is contained in:
Will Murphy
2026-03-18 10:53:51 -04:00
committed by GitHub
parent a3dacf5ecd
commit e38851143e
56 changed files with 704 additions and 214 deletions
@@ -270,7 +270,7 @@ func (c *NVDAPIClient) fetchPage(ctx context.Context, startIndex int, start, end
// handleRateLimit handles HTTP 429 responses by parsing Retry-After and waiting
func (c *NVDAPIClient) handleRateLimit(ctx context.Context, httpResp *http.Response, attempt int) error {
body, _ := io.ReadAll(httpResp.Body)
body, _ := io.ReadAll(httpResp.Body) //nolint:gocritic // offline code generator
httpResp.Body.Close()
// parse Retry-After header
@@ -298,7 +298,7 @@ func checkHTTPStatus(httpResp *http.Response) error {
if httpResp.StatusCode == http.StatusOK {
return nil
}
body, _ := io.ReadAll(httpResp.Body)
body, _ := io.ReadAll(httpResp.Body) //nolint:gocritic // offline code generator
httpResp.Body.Close()
return fmt.Errorf("NVD API error (status %d): %s", httpResp.StatusCode, string(body))
}
@@ -24,6 +24,7 @@ import (
"github.com/anchore/syft/internal/cmptest"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
@@ -62,7 +63,7 @@ type CatalogTester struct {
skipTestObservations bool
}
func Context() context.Context {
func initLicenseScanner() {
once.Do(func() {
// most of the time in testing is initializing the scanner. Let's do that just once
sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75}
@@ -72,13 +73,20 @@ func Context() context.Context {
}
licenseScanner = &scanner
})
}
return licenses.SetContextLicenseScanner(context.Background(), *licenseScanner)
// Context returns a context with a shared license scanner and a TempDir backed by t.TempDir(),
// so cleanup is handled automatically when the test finishes.
func Context(t *testing.T) context.Context {
t.Helper()
initLicenseScanner()
td := tmpdir.FromPath(t.TempDir())
ctx := tmpdir.WithValue(context.Background(), td)
return licenses.SetContextLicenseScanner(ctx, *licenseScanner)
}
func NewCatalogTester() *CatalogTester {
return &CatalogTester{
context: Context(),
locationComparer: cmptest.DefaultLocationComparer,
licenseComparer: cmptest.DefaultLicenseComparer,
packageStringer: stringPackage,
@@ -272,9 +280,18 @@ func (p *CatalogTester) WithoutTestObserver() *CatalogTester {
return p
}
func (p *CatalogTester) ensureContext(t *testing.T) context.Context {
t.Helper()
if p.context != nil {
return p.context
}
return Context(t)
}
func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
t.Helper()
pkgs, relationships, err := parser(p.context, p.resolver, p.env, p.reader)
ctx := p.ensureContext(t)
pkgs, relationships, err := parser(ctx, p.resolver, p.env, p.reader)
// only test for errors if explicitly requested
if p.wantErr != nil {
@@ -289,10 +306,11 @@ func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
t.Helper()
ctx := p.ensureContext(t)
resolver := NewObservingResolver(p.resolver)
pkgs, relationships, err := cataloger.Catalog(p.context, resolver)
pkgs, relationships, err := cataloger.Catalog(ctx, resolver)
// this is a minimum set, the resolver may return more that just this list
for _, path := range p.expectedPathResponses {