diff --git a/syft/pkg/cataloger/haskell/cataloger.go b/syft/pkg/cataloger/haskell/cataloger.go index 2841792080c..a7638ee837c 100644 --- a/syft/pkg/cataloger/haskell/cataloger.go +++ b/syft/pkg/cataloger/haskell/cataloger.go @@ -1,15 +1,17 @@ package haskell import ( - "github.com/anchore/syft/syft/pkg/cataloger/common" + "github.com/anchore/syft/syft/pkg/cataloger/generic" ) +// TODO: it seems that the stack.yaml/stack.lock/cabal.project.freeze have different purposes and could have different installation intentions +// (some describe intent and are meant to be used by a tool to resolve more dependencies while others describe the actual installed state). +// This hints at splittin these into multiple catalogers, but for now we'll keep them together. + // NewHackageCataloger returns a new Haskell cataloger object. -func NewHackageCataloger() *common.GenericCataloger { - globParsers := map[string]common.ParserFn{ - "**/stack.yaml": parseStackYaml, - "**/stack.yaml.lock": parseStackLock, - "**/cabal.project.freeze": parseCabalFreeze, - } - return common.NewGenericCataloger(nil, globParsers, "hackage-cataloger") +func NewHackageCataloger() *generic.Cataloger { + return generic.NewCataloger("haskell-cataloger"). + WithParserByGlobs(parseStackYaml, "**/stack.yaml"). + WithParserByGlobs(parseStackLock, "**/stack.yaml.lock"). + WithParserByGlobs(parseCabalFreeze, "**/cabal.project.freeze") } diff --git a/syft/pkg/cataloger/haskell/package.go b/syft/pkg/cataloger/haskell/package.go new file mode 100644 index 00000000000..c7c1aa1581a --- /dev/null +++ b/syft/pkg/cataloger/haskell/package.go @@ -0,0 +1,40 @@ +package haskell + +import ( + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func newPackage(name, version string, m *pkg.HackageMetadata, locations ...source.Location) pkg.Package { + p := pkg.Package{ + Name: name, + Version: version, + Locations: source.NewLocationSet(locations...), + PURL: packageURL(name, version), + Language: pkg.Haskell, + Type: pkg.HackagePkg, + } + + if m != nil { + p.MetadataType = pkg.HackageMetadataType + p.Metadata = *m + } + + p.SetID() + + return p +} + +func packageURL(name, version string) string { + var qualifiers packageurl.Qualifiers + + return packageurl.NewPackageURL( + packageurl.TypeHackage, + "", + name, + version, + qualifiers, + "", + ).ToString() +} diff --git a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go index e09a1a7e927..b3e9c9c8061 100644 --- a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go +++ b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go @@ -9,16 +9,16 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/pkg/cataloger/common" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" ) -// integrity check -var _ common.ParserFn = parseCabalFreeze +var _ generic.Parser = parseCabalFreeze // parseCabalFreeze is a parser function for cabal.project.freeze contents, returning all packages discovered. -func parseCabalFreeze(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { +func parseCabalFreeze(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { r := bufio.NewReader(reader) - pkgs := []*pkg.Package{} + var pkgs []pkg.Package for { line, err := r.ReadString('\n') switch { @@ -35,19 +35,9 @@ func parseCabalFreeze(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Re line = strings.TrimSpace(line) startPkgEncoding, endPkgEncoding := strings.Index(line, "any.")+4, strings.Index(line, ",") line = line[startPkgEncoding:endPkgEncoding] - splits := strings.Split(line, " ==") + fields := strings.Split(line, " ==") - pkgName, pkgVersion := splits[0], splits[1] - pkgs = append(pkgs, &pkg.Package{ - Name: pkgName, - Version: pkgVersion, - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: pkgName, - Version: pkgVersion, - }, - }) + pkgName, pkgVersion := fields[0], fields[1] + pkgs = append(pkgs, newPackage(pkgName, pkgVersion, nil, reader.Location)) } } diff --git a/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go b/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go index 3f898dffff7..2c4a96c77b8 100644 --- a/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go +++ b/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go @@ -1,152 +1,111 @@ package haskell import ( - "os" "testing" - "github.com/go-test/deep" - + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" ) func TestParseCabalFreeze(t *testing.T) { - expected := []*pkg.Package{ + fixture := "test-fixtures/cabal.project.freeze" + locationSet := source.NewLocationSet(source.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ { - Name: "Cabal", - Version: "3.2.1.0", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "Cabal", - Version: "3.2.1.0", - }, + Name: "Cabal", + Version: "3.2.1.0", + PURL: "pkg:hackage/Cabal@3.2.1.0", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "Diff", - Version: "0.4.1", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "Diff", - Version: "0.4.1", - }, + Name: "Diff", + Version: "0.4.1", + PURL: "pkg:hackage/Diff@0.4.1", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "HTTP", - Version: "4000.3.16", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "HTTP", - Version: "4000.3.16", - }, + Name: "HTTP", + Version: "4000.3.16", + PURL: "pkg:hackage/HTTP@4000.3.16", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "HUnit", - Version: "1.6.2.0", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "HUnit", - Version: "1.6.2.0", - }, + Name: "HUnit", + Version: "1.6.2.0", + PURL: "pkg:hackage/HUnit@1.6.2.0", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "OneTuple", - Version: "0.3.1", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "OneTuple", - Version: "0.3.1", - }, + Name: "OneTuple", + Version: "0.3.1", + PURL: "pkg:hackage/OneTuple@0.3.1", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "Only", - Version: "0.1", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "Only", - Version: "0.1", - }, + Name: "Only", + Version: "0.1", + PURL: "pkg:hackage/Only@0.1", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "PyF", - Version: "0.10.2.0", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "PyF", - Version: "0.10.2.0", - }, + Name: "PyF", + Version: "0.10.2.0", + PURL: "pkg:hackage/PyF@0.10.2.0", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "QuickCheck", - Version: "2.14.2", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "QuickCheck", - Version: "2.14.2", - }, + Name: "QuickCheck", + Version: "2.14.2", + PURL: "pkg:hackage/QuickCheck@2.14.2", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "RSA", - Version: "2.4.1", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "RSA", - Version: "2.4.1", - }, + Name: "RSA", + Version: "2.4.1", + PURL: "pkg:hackage/RSA@2.4.1", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "SHA", - Version: "1.6.4.4", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "SHA", - Version: "1.6.4.4", - }, + Name: "SHA", + Version: "1.6.4.4", + PURL: "pkg:hackage/SHA@1.6.4.4", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, { - Name: "Spock", - Version: "0.14.0.0", - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: "Spock", - Version: "0.14.0.0", - }, + Name: "Spock", + Version: "0.14.0.0", + PURL: "pkg:hackage/Spock@0.14.0.0", + Locations: locationSet, + Language: pkg.Haskell, + Type: pkg.HackagePkg, }, } - fixture, err := os.Open("test-fixtures/cabal.project.freeze") - if err != nil { - t.Fatalf("failed to open fixture: %+v", err) - } - - // TODO: no relationships are under test yet - actual, _, err := parseCabalFreeze(fixture.Name(), fixture) - if err != nil { - t.Error(err) - } + // TODO: relationships are not under test yet + var expectedRelationships []artifact.Relationship - differences := deep.Equal(expected, actual) - if differences != nil { - t.Errorf("returned package list differed from expectation: %+v", differences) - } + pkgtest.TestFileParser(t, fixture, parseCabalFreeze, expectedPkgs, expectedRelationships) } diff --git a/syft/pkg/cataloger/haskell/parse_stack_lock.go b/syft/pkg/cataloger/haskell/parse_stack_lock.go index 0cbbda80b60..58ce52c30cb 100644 --- a/syft/pkg/cataloger/haskell/parse_stack_lock.go +++ b/syft/pkg/cataloger/haskell/parse_stack_lock.go @@ -9,11 +9,11 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/pkg/cataloger/common" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" ) -// integrity check -var _ common.ParserFn = parseStackLock +var _ generic.Parser = parseStackLock type stackLock struct { Packages []stackPackage `yaml:"packages"` @@ -37,19 +37,8 @@ type completedSnapshot struct { Sha string `yaml:"sha256"` } -func parseStackPackageEncoding(pkgEncoding string) (name, version, hash string) { - lastDashIdx := strings.LastIndex(pkgEncoding, "-") - name = pkgEncoding[:lastDashIdx] - remainingEncoding := pkgEncoding[lastDashIdx+1:] - encodingSplits := strings.Split(remainingEncoding, "@") - version = encodingSplits[0] - startHash, endHash := strings.Index(encodingSplits[1], ":")+1, strings.Index(encodingSplits[1], ",") - hash = encodingSplits[1][startHash:endHash] - return -} - // parseStackLock is a parser function for stack.yaml.lock contents, returning all packages discovered. -func parseStackLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { +func parseStackLock(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { bytes, err := io.ReadAll(reader) if err != nil { return nil, nil, fmt.Errorf("failed to load stack.yaml.lock file: %w", err) @@ -62,30 +51,32 @@ func parseStackLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela } var ( - pkgs []*pkg.Package + pkgs []pkg.Package snapshotURL string ) for _, snap := range lockFile.Snapshots { + // TODO: handle multiple snapshots (split the metadata struct into more distinct structs and types) snapshotURL = snap.Completed.URL } for _, pack := range lockFile.Packages { pkgName, pkgVersion, pkgHash := parseStackPackageEncoding(pack.Completed.Hackage) - pkgs = append(pkgs, &pkg.Package{ - Name: pkgName, - Version: pkgVersion, - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: pkgName, - Version: pkgVersion, - PkgHash: &pkgHash, - SnapshotURL: &snapshotURL, - }, - }) + pkgs = append(pkgs, newPackage(pkgName, pkgVersion, &pkg.HackageMetadata{ + PkgHash: pkgHash, + SnapshotURL: snapshotURL, + }, reader.Location)) } return pkgs, nil, nil } +func parseStackPackageEncoding(pkgEncoding string) (name, version, hash string) { + lastDashIdx := strings.LastIndex(pkgEncoding, "-") + name = pkgEncoding[:lastDashIdx] + remainingEncoding := pkgEncoding[lastDashIdx+1:] + encodingSplits := strings.Split(remainingEncoding, "@") + version = encodingSplits[0] + startHash, endHash := strings.Index(encodingSplits[1], ":")+1, strings.Index(encodingSplits[1], ",") + hash = encodingSplits[1][startHash:endHash] + return +} diff --git a/syft/pkg/cataloger/haskell/parse_stack_lock_test.go b/syft/pkg/cataloger/haskell/parse_stack_lock_test.go index 0f1cfde40ae..2cdfbc75b86 100644 --- a/syft/pkg/cataloger/haskell/parse_stack_lock_test.go +++ b/syft/pkg/cataloger/haskell/parse_stack_lock_test.go @@ -1,153 +1,141 @@ package haskell import ( - "os" "testing" - "github.com/go-test/deep" - + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" ) -func fixtureP(str string) *string { - return &str -} - func TestParseStackLock(t *testing.T) { url := "https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml" - expected := []*pkg.Package{ + fixture := "test-fixtures/stack.yaml.lock" + locationSet := source.NewLocationSet(source.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ { Name: "HTTP", Version: "4000.3.16", + PURL: "pkg:hackage/HTTP@4000.3.16", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "HTTP", - Version: "4000.3.16", - PkgHash: fixtureP("6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71"), - SnapshotURL: &url, + PkgHash: "6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71", + SnapshotURL: url, }, }, { Name: "configurator-pg", Version: "0.2.6", + PURL: "pkg:hackage/configurator-pg@0.2.6", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "configurator-pg", - Version: "0.2.6", - PkgHash: fixtureP("cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be"), - SnapshotURL: &url, + PkgHash: "cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be", + SnapshotURL: url, }, }, { Name: "hasql-dynamic-statements", Version: "0.3.1.1", + PURL: "pkg:hackage/hasql-dynamic-statements@0.3.1.1", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "hasql-dynamic-statements", - Version: "0.3.1.1", - PkgHash: fixtureP("2cfe6e75990e690f595a87cbe553f2e90fcd738610f6c66749c81cc4396b2cc4"), - SnapshotURL: &url, + PkgHash: "2cfe6e75990e690f595a87cbe553f2e90fcd738610f6c66749c81cc4396b2cc4", + SnapshotURL: url, }, }, { Name: "hasql-implicits", Version: "0.1.0.4", + PURL: "pkg:hackage/hasql-implicits@0.1.0.4", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "hasql-implicits", - Version: "0.1.0.4", - PkgHash: fixtureP("0848d3cbc9d94e1e539948fa0be4d0326b26335034161bf8076785293444ca6f"), - SnapshotURL: &url, + PkgHash: "0848d3cbc9d94e1e539948fa0be4d0326b26335034161bf8076785293444ca6f", + SnapshotURL: url, }, }, { Name: "hasql-pool", Version: "0.5.2.2", + PURL: "pkg:hackage/hasql-pool@0.5.2.2", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "hasql-pool", - Version: "0.5.2.2", - PkgHash: fixtureP("b56d4dea112d97a2ef4b2749508c0ca646828cb2d77b827e8dc433d249bb2062"), - SnapshotURL: &url, + PkgHash: "b56d4dea112d97a2ef4b2749508c0ca646828cb2d77b827e8dc433d249bb2062", + SnapshotURL: url, }, }, { Name: "lens-aeson", Version: "1.1.3", + PURL: "pkg:hackage/lens-aeson@1.1.3", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "lens-aeson", - Version: "1.1.3", - PkgHash: fixtureP("52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050"), - SnapshotURL: &url, + PkgHash: "52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050", + SnapshotURL: url, }, }, { Name: "optparse-applicative", Version: "0.16.1.0", + PURL: "pkg:hackage/optparse-applicative@0.16.1.0", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "optparse-applicative", - Version: "0.16.1.0", - PkgHash: fixtureP("418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731"), - SnapshotURL: &url, + PkgHash: "418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731", + SnapshotURL: url, }, }, { Name: "protolude", Version: "0.3.2", + PURL: "pkg:hackage/protolude@0.3.2", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "protolude", - Version: "0.3.2", - PkgHash: fixtureP("2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1"), - SnapshotURL: &url, + PkgHash: "2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1", + SnapshotURL: url, }, }, { Name: "ptr", Version: "0.16.8.2", + PURL: "pkg:hackage/ptr@0.16.8.2", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "ptr", - Version: "0.16.8.2", - PkgHash: fixtureP("708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac"), - SnapshotURL: &url, + PkgHash: "708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac", + SnapshotURL: url, }, }, } - fixture, err := os.Open("test-fixtures/stack.yaml.lock") - if err != nil { - t.Fatalf("failed to open fixture: %+v", err) - } - - // TODO: no relationships are under test yet - actual, _, err := parseStackLock(fixture.Name(), fixture) - if err != nil { - t.Error(err) - } + // TODO: relationships are not under test yet + var expectedRelationships []artifact.Relationship - differences := deep.Equal(expected, actual) - if differences != nil { - t.Errorf("returned package list differed from expectation: %+v", differences) - } + pkgtest.TestFileParser(t, fixture, parseStackLock, expectedPkgs, expectedRelationships) } diff --git a/syft/pkg/cataloger/haskell/parse_stack_yaml.go b/syft/pkg/cataloger/haskell/parse_stack_yaml.go index 3fceb15cc37..7325c8b798f 100644 --- a/syft/pkg/cataloger/haskell/parse_stack_yaml.go +++ b/syft/pkg/cataloger/haskell/parse_stack_yaml.go @@ -8,18 +8,18 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/pkg/cataloger/common" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" ) -// integrity check -var _ common.ParserFn = parseStackYaml +var _ generic.Parser = parseStackYaml type stackYaml struct { ExtraDeps []string `yaml:"extra-deps"` } // parseStackYaml is a parser function for stack.yaml contents, returning all packages discovered. -func parseStackYaml(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { +func parseStackYaml(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { bytes, err := io.ReadAll(reader) if err != nil { return nil, nil, fmt.Errorf("failed to load stack.yaml file: %w", err) @@ -31,24 +31,12 @@ func parseStackYaml(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela return nil, nil, fmt.Errorf("failed to parse stack.yaml file: %w", err) } - var ( - pkgs []*pkg.Package - ) - + var pkgs []pkg.Package for _, dep := range stackFile.ExtraDeps { pkgName, pkgVersion, pkgHash := parseStackPackageEncoding(dep) - pkgs = append(pkgs, &pkg.Package{ - Name: pkgName, - Version: pkgVersion, - Language: pkg.Haskell, - Type: pkg.HackagePkg, - MetadataType: pkg.HackageMetadataType, - Metadata: pkg.HackageMetadata{ - Name: pkgName, - Version: pkgVersion, - PkgHash: &pkgHash, - }, - }) + pkgs = append(pkgs, newPackage(pkgName, pkgVersion, &pkg.HackageMetadata{ + PkgHash: pkgHash, + }, reader.Location)) } return pkgs, nil, nil diff --git a/syft/pkg/cataloger/haskell/parse_stack_yaml_test.go b/syft/pkg/cataloger/haskell/parse_stack_yaml_test.go index cdf7c0359ab..1e035a7a60e 100644 --- a/syft/pkg/cataloger/haskell/parse_stack_yaml_test.go +++ b/syft/pkg/cataloger/haskell/parse_stack_yaml_test.go @@ -1,127 +1,120 @@ package haskell import ( - "os" "testing" - "github.com/go-test/deep" - + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" ) func TestParseStackYaml(t *testing.T) { - expected := []*pkg.Package{ + fixture := "test-fixtures/stack.yaml" + locationSet := source.NewLocationSet(source.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ { Name: "ShellCheck", Version: "0.8.0", + PURL: "pkg:hackage/ShellCheck@0.8.0", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "ShellCheck", - Version: "0.8.0", - PkgHash: fixtureP("353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df"), + PkgHash: "353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df", }, }, { Name: "colourista", Version: "0.1.0.1", + PURL: "pkg:hackage/colourista@0.1.0.1", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "colourista", - Version: "0.1.0.1", - PkgHash: fixtureP("98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e"), + PkgHash: "98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e", }, }, { Name: "language-docker", Version: "11.0.0", + PURL: "pkg:hackage/language-docker@11.0.0", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "language-docker", - Version: "11.0.0", - PkgHash: fixtureP("3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55"), + PkgHash: "3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55", }, }, { Name: "spdx", Version: "1.0.0.2", + PURL: "pkg:hackage/spdx@1.0.0.2", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "spdx", - Version: "1.0.0.2", - PkgHash: fixtureP("7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed"), + PkgHash: "7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed", }, }, { Name: "hspec", Version: "2.9.4", + PURL: "pkg:hackage/hspec@2.9.4", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "hspec", - Version: "2.9.4", - PkgHash: fixtureP("658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3"), + PkgHash: "658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3", }, }, { Name: "hspec-core", Version: "2.9.4", + PURL: "pkg:hackage/hspec-core@2.9.4", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "hspec-core", - Version: "2.9.4", - PkgHash: fixtureP("a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8"), + PkgHash: "a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8", }, }, { Name: "hspec-discover", Version: "2.9.4", + PURL: "pkg:hackage/hspec-discover@2.9.4", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "hspec-discover", - Version: "2.9.4", - PkgHash: fixtureP("fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6"), + PkgHash: "fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6", }, }, { Name: "stm", Version: "2.5.0.2", + PURL: "pkg:hackage/stm@2.5.0.2", + Locations: locationSet, Language: pkg.Haskell, Type: pkg.HackagePkg, MetadataType: pkg.HackageMetadataType, Metadata: pkg.HackageMetadata{ - Name: "stm", - Version: "2.5.0.2", - PkgHash: fixtureP("e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55"), + PkgHash: "e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55", }, }, } - fixture, err := os.Open("test-fixtures/stack.yaml") - if err != nil { - t.Fatalf("failed to open fixture: %+v", err) - } + // TODO: relationships are not under test yet + var expectedRelationships []artifact.Relationship - // TODO: no relationships are under test yet - actual, _, err := parseStackYaml(fixture.Name(), fixture) - if err != nil { - t.Error(err) - } + pkgtest.TestFileParser(t, fixture, parseStackYaml, expectedPkgs, expectedRelationships) - differences := deep.Equal(expected, actual) - if differences != nil { - t.Errorf("returned package list differed from expectation: %+v", differences) - } } diff --git a/syft/pkg/hackage_metadata.go b/syft/pkg/hackage_metadata.go index 0f8c3801195..3d691273f3f 100644 --- a/syft/pkg/hackage_metadata.go +++ b/syft/pkg/hackage_metadata.go @@ -1,28 +1,8 @@ package pkg -import ( - "github.com/anchore/packageurl-go" - "github.com/anchore/syft/syft/linux" -) - -var _ urlIdentifier = (*HackageMetadata)(nil) - type HackageMetadata struct { - Name string `mapstructure:"name" json:"name"` - Version string `mapstructure:"version" json:"version"` - PkgHash *string `mapstructure:"pkgHash" json:"pkgHash,omitempty"` - SnapshotURL *string `mapstructure:"snapshotURL" json:"snapshotURL,omitempty"` -} - -func (m HackageMetadata) PackageURL(_ *linux.Release) string { - var qualifiers packageurl.Qualifiers - - return packageurl.NewPackageURL( - packageurl.TypeHackage, - "", - m.Name, - m.Version, - qualifiers, - "", - ).ToString() + Name string `mapstructure:"name" json:"name"` + Version string `mapstructure:"version" json:"version"` + PkgHash string `mapstructure:"pkgHash" json:"pkgHash,omitempty"` + SnapshotURL string `mapstructure:"snapshotURL" json:"snapshotURL,omitempty"` } diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index b9f7bd53077..551b6b944fc 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -132,21 +132,6 @@ func TestPackageURL(t *testing.T) { }, expected: "pkg:cocoapods/GlossButtonNode@3.1.2", }, - { - name: "hackage", - pkg: Package{ - Name: "HTTP", - Version: "4000.3.16", - Type: HackagePkg, - Language: Haskell, - MetadataType: HackageMetadataType, - Metadata: HackageMetadata{ - Name: "HTTP", - Version: "4000.3.16", - }, - }, - expected: "pkg:hackage/HTTP@4000.3.16", - }, } var pkgTypes []string @@ -165,6 +150,7 @@ func TestPackageURL(t *testing.T) { expectedTypes.Remove(string(DotnetPkg)) expectedTypes.Remove(string(DebPkg)) expectedTypes.Remove(string(GoModulePkg)) + expectedTypes.Remove(string(HackagePkg)) for _, test := range tests { t.Run(test.name, func(t *testing.T) {