diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d10a9520022..896889aabd6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -99,16 +99,12 @@ brews: dockers: - image_templates: - - "anchore/syft:latest" - - "anchore/syft:{{ .Tag }}-amd64" - - "anchore/syft:v{{ .Major }}-amd64" - - "anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64" - - "ghcr.io/anchore/syft:latest" - - "ghcr.io/anchore/syft:{{ .Tag }}-amd64" - - "ghcr.io/anchore/syft:v{{ .Major }}-amd64" - - "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64" + - anchore/syft:debug + - anchore/syft:{{.Tag}}-debug + - ghcr.io/anchore/syft:debug + - ghcr.io/anchore/syft:{{.Tag}}-debug goarch: amd64 - dockerfile: Dockerfile + dockerfile: Dockerfile.debug use: buildx build_flag_templates: - "--platform=linux/amd64" @@ -118,14 +114,12 @@ dockers: - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - - "anchore/syft:{{ .Tag }}-arm64v8" - - "anchore/syft:v{{ .Major }}-arm64v8" - - "anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8" - - "ghcr.io/anchore/syft:{{ .Tag }}-arm64v8" - - "ghcr.io/anchore/syft:v{{ .Major }}-arm64v8" - - "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8" + - anchore/syft:debug-arm64v8 + - anchore/syft:{{.Tag}}-debug-arm64v8 + - ghcr.io/anchore/syft:debug-arm64v8 + - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8 goarch: arm64 - dockerfile: Dockerfile + dockerfile: Dockerfile.debug use: buildx build_flag_templates: - "--platform=linux/arm64/v8" @@ -133,42 +127,61 @@ dockers: - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - + + - image_templates: + - anchore/syft:latest + - anchore/syft:{{.Tag}} + - ghcr.io/anchore/syft:latest + - ghcr.io/anchore/syft:{{.Tag}} + goarch: amd64 + dockerfile: Dockerfile + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + - image_templates: - - "anchore/syft:{{ .Tag }}-ppc64le" - - "anchore/syft:v{{ .Major }}-ppc64le" - - "anchore/syft:v{{ .Major }}.{{ .Minor }}-ppc64le" - - "ghcr.io/anchore/syft:{{ .Tag }}-ppc64le" - - "ghcr.io/anchore/syft:v{{ .Major }}-ppc64le" - - "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-ppc64le" - goarch: ppc64le + - anchore/syft:{{.Tag}}-arm64v8 + - ghcr.io/anchore/syft:{{.Tag}}-arm64v8 + goarch: arm64 dockerfile: Dockerfile use: buildx build_flag_templates: - - "--platform=linux/ppc64le" + - "--platform=linux/arm64/v8" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" docker_manifests: - - name_template: anchore/syft:{{ .Tag }} - image_templates: - - anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64 - - anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8 - - anchore/syft:v{{ .Major }}.{{ .Minor }}-ppc64le - name_template: anchore/syft:latest image_templates: - - anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64 - - anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8 - - anchore/syft:v{{ .Major }}.{{ .Minor }}-ppc64le - - name_template: ghcr.io/anchore/syft:{{ .Tag }} + - anchore/syft:{{.Tag}} + - anchore/syft:{{.Tag}}-arm64v8 + + - name_template: anchore/syft:debug + - anchore/syft:{{.Tag}}-debug + - anchore/syft:{{.Tag}}-debug-arm64v8 + + - name_template: anchore/syft:{{.Tag}} image_templates: - - ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64 - - ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8 - - ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-ppc64le + - anchore/syft:{{.Tag}} + - anchore/syft:{{.Tag}}-arm64v8 + - name_template: ghcr.io/anchore/syft:latest image_templates: - - ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64 - - ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8 - - ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-ppc64le + - anchore/syft:{{.Tag}} + - anchore/syft:{{.Tag}}-arm64v8 + + - name_template: ghcr.io/anchore/syft:debug + image_templates: + - ghcr.io/anchore/syft:{{.Tag}}-debug + - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8 + + - name_template: ghcr.io/anchore/syft:{{.Tag}} + image_templates: + - ghcr.io/anchore/syft:{{.Tag}} + - ghcr.io/anchore/syft:{{.Tag}}-arm64v8 diff --git a/Dockerfile b/Dockerfile index 697d148808b..c93c21fabf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,4 @@ -FROM alpine:latest AS build - -RUN apk --no-cache add ca-certificates +FROM gcr.io/distroless/static-debian11:debug AS build FROM scratch # needed for version check HTTPS request @@ -27,6 +25,5 @@ LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md" LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" LABEL io.artifacthub.package.license="Apache-2.0" - ENTRYPOINT ["/syft"] diff --git a/Dockerfile.debug b/Dockerfile.debug new file mode 100644 index 00000000000..3c1761cf41d --- /dev/null +++ b/Dockerfile.debug @@ -0,0 +1,25 @@ +FROM gcr.io/distroless/static-debian11:debug + +# create the /tmp dir, which is needed for image content cache +WORKDIR /tmp + +COPY syft / + +ARG BUILD_DATE +ARG BUILD_VERSION +ARG VCS_REF +ARG VCS_URL + +LABEL org.opencontainers.image.created=$BUILD_DATE +LABEL org.opencontainers.image.title="syft" +LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems" +LABEL org.opencontainers.image.source=$VCS_URL +LABEL org.opencontainers.image.revision=$VCS_REF +LABEL org.opencontainers.image.vendor="Anchore, Inc." +LABEL org.opencontainers.image.version=$BUILD_VERSION +LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md" +LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" +LABEL io.artifacthub.package.license="Apache-2.0" + +ENTRYPOINT ["/syft"] diff --git a/Makefile b/Makefile index 5a4d2ab580f..5cd41a213c9 100644 --- a/Makefile +++ b/Makefile @@ -369,7 +369,7 @@ clean-test-image-tar-cache: ## Delete all test cache (built docker image tars) .PHONY: clear-test-image-docker-cache clean-test-image-docker-cache: ## Purge all test docker images - docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs docker rmi --force + docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force .PHONY: show-test-image-cache show-test-image-cache: ## Show all docker and image tar cache diff --git a/README.md b/README.md index 2a7384cd331..f0dc6dc5280 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ A CLI tool and Go library for generating a Software Bill of Materials (SBOM) fro - Dotnet (deps.json) - Objective-C (cocoapods) - Go (go.mod, Go binaries) +- Haskell (cabal, stack) - Java (jar, ear, war, par, sar) - JavaScript (npm, yarn) - Jenkins Plugins (jpi, hpi) diff --git a/cmd/syft/cli/options/writer.go b/cmd/syft/cli/options/writer.go index 85a704fb356..2b28dd97a69 100644 --- a/cmd/syft/cli/options/writer.go +++ b/cmd/syft/cli/options/writer.go @@ -53,7 +53,7 @@ func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out [ format := syft.FormatByName(name) if format == nil { - errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name)) + errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, FormatAliases(syft.FormatIDs()...))) continue } diff --git a/cmd/syft/cli/options/writer_test.go b/cmd/syft/cli/options/writer_test.go index 00e096e8fed..25a01fde952 100644 --- a/cmd/syft/cli/options/writer_test.go +++ b/cmd/syft/cli/options/writer_test.go @@ -22,7 +22,7 @@ func TestIsSupportedFormat(t *testing.T) { { outputs: []string{"unknown"}, wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool { - return assert.ErrorContains(t, err, "bad output format: 'unknown'") + return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`) }, }, } diff --git a/cmd/syft/cli/packages.go b/cmd/syft/cli/packages.go index d9ccecded2a..aff2d28c224 100644 --- a/cmd/syft/cli/packages.go +++ b/cmd/syft/cli/packages.go @@ -13,7 +13,7 @@ import ( ) const ( - packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages + packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages {{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details {{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM {{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM diff --git a/internal/formats/common/spdxhelpers/source_info.go b/internal/formats/common/spdxhelpers/source_info.go index c330ea8c997..81c63348d65 100644 --- a/internal/formats/common/spdxhelpers/source_info.go +++ b/internal/formats/common/spdxhelpers/source_info.go @@ -6,6 +6,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) +//nolint:funlen func SourceInfo(p pkg.Package) string { answer := "" switch p.Type { @@ -41,6 +42,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from conan manifest" case pkg.PortagePkg: answer = "acquired package info from portage DB" + case pkg.HackagePkg: + answer = "acquired package info from cabal or stack manifest files" default: answer = "acquired package info from the following paths" } diff --git a/internal/formats/common/spdxhelpers/source_info_test.go b/internal/formats/common/spdxhelpers/source_info_test.go index 86a89318141..a403aed054a 100644 --- a/internal/formats/common/spdxhelpers/source_info_test.go +++ b/internal/formats/common/spdxhelpers/source_info_test.go @@ -174,6 +174,14 @@ func Test_SourceInfo(t *testing.T) { "from portage DB", }, }, + { + input: pkg.Package{ + Type: pkg.HackagePkg, + }, + expected: []string{ + "from cabal or stack manifest files", + }, + }, } var pkgTypes []pkg.Type for _, test := range tests { diff --git a/internal/formats/syftjson/model/package.go b/internal/formats/syftjson/model/package.go index 43a1d4520b8..09fdf07ef5e 100644 --- a/internal/formats/syftjson/model/package.go +++ b/internal/formats/syftjson/model/package.go @@ -169,6 +169,12 @@ func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error { return err } p.Metadata = payload + case pkg.HackageMetadataType: + var payload pkg.HackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload default: log.Warnf("unknown package metadata type=%q for packageID=%q", p.MetadataType, p.ID) } diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 06198b2863d..98d98436d2d 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -18,6 +18,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/deb" "github.com/anchore/syft/syft/pkg/cataloger/dotnet" "github.com/anchore/syft/syft/pkg/cataloger/golang" + "github.com/anchore/syft/syft/pkg/cataloger/haskell" "github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/javascript" "github.com/anchore/syft/syft/pkg/cataloger/php" @@ -82,6 +83,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger { swift.NewCocoapodsCataloger(), cpp.NewConanfileCataloger(), portage.NewPortageCataloger(), + haskell.NewHackageCataloger(), }, cfg.Catalogers) } @@ -110,6 +112,7 @@ func AllCatalogers(cfg Config) []Cataloger { swift.NewCocoapodsCataloger(), cpp.NewConanfileCataloger(), portage.NewPortageCataloger(), + haskell.NewHackageCataloger(), }, cfg.Catalogers) } diff --git a/syft/pkg/cataloger/haskell/cataloger.go b/syft/pkg/cataloger/haskell/cataloger.go new file mode 100644 index 00000000000..2841792080c --- /dev/null +++ b/syft/pkg/cataloger/haskell/cataloger.go @@ -0,0 +1,15 @@ +package haskell + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/common" +) + +// 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") +} diff --git a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go new file mode 100644 index 00000000000..e09a1a7e927 --- /dev/null +++ b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go @@ -0,0 +1,53 @@ +package haskell + +import ( + "bufio" + "errors" + "fmt" + "io" + "strings" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common" +) + +// integrity check +var _ common.ParserFn = 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) { + r := bufio.NewReader(reader) + pkgs := []*pkg.Package{} + for { + line, err := r.ReadString('\n') + switch { + case errors.Is(io.EOF, err): + return pkgs, nil, nil + case err != nil: + return nil, nil, fmt.Errorf("failed to parse cabal.project.freeze file: %w", err) + } + + if !strings.Contains(line, "any.") { + continue + } + + line = strings.TrimSpace(line) + startPkgEncoding, endPkgEncoding := strings.Index(line, "any.")+4, strings.Index(line, ",") + line = line[startPkgEncoding:endPkgEncoding] + splits := 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, + }, + }) + } +} diff --git a/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go b/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go new file mode 100644 index 00000000000..162d0d8cb4b --- /dev/null +++ b/syft/pkg/cataloger/haskell/parse_cabal_freeze_test.go @@ -0,0 +1,151 @@ +package haskell + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/go-test/deep" +) + +func TestParseCabalFreeze(t *testing.T) { + expected := []*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: "Diff", + Version: "0.4.1", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "Diff", + Version: "0.4.1", + }, + }, + { + 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: "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: "OneTuple", + Version: "0.3.1", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "OneTuple", + Version: "0.3.1", + }, + }, + { + Name: "Only", + Version: "0.1", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "Only", + Version: "0.1", + }, + }, + { + 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: "QuickCheck", + Version: "2.14.2", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "QuickCheck", + Version: "2.14.2", + }, + }, + { + 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: "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: "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", + }, + }, + } + + 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) + } + + differences := deep.Equal(expected, actual) + if differences != nil { + t.Errorf("returned package list differed from expectation: %+v", differences) + } +} diff --git a/syft/pkg/cataloger/haskell/parse_stack_lock.go b/syft/pkg/cataloger/haskell/parse_stack_lock.go new file mode 100644 index 00000000000..0d08c25ffa6 --- /dev/null +++ b/syft/pkg/cataloger/haskell/parse_stack_lock.go @@ -0,0 +1,90 @@ +package haskell + +import ( + "fmt" + "io" + "strings" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common" + "gopkg.in/yaml.v3" +) + +// integrity check +var _ common.ParserFn = parseStackLock + +type stackLock struct { + Packages []stackPackage `yaml:"packages"` + Snapshots []stackSnapshot `yaml:"snapshots"` +} + +type stackPackage struct { + Completed completedPackage `yaml:"completed"` +} + +type completedPackage struct { + Hackage string `yaml:"hackage"` +} + +type stackSnapshot struct { + Completed completedSnapshot `yaml:"completed"` +} + +type completedSnapshot struct { + URL string `yaml:"url"` + 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) { + bytes, err := io.ReadAll(reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to load stack.yaml.lock file: %w", err) + } + + var lockFile stackLock + + if err := yaml.Unmarshal(bytes, &lockFile); err != nil { + return nil, nil, fmt.Errorf("failed to parse stack.yaml.lock file: %w", err) + } + + var ( + pkgs []*pkg.Package + snapshotURL string + ) + + for _, snap := range lockFile.Snapshots { + 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, + }, + }) + } + + return pkgs, nil, nil +} diff --git a/syft/pkg/cataloger/haskell/parse_stack_lock_test.go b/syft/pkg/cataloger/haskell/parse_stack_lock_test.go new file mode 100644 index 00000000000..3ad296b2c9b --- /dev/null +++ b/syft/pkg/cataloger/haskell/parse_stack_lock_test.go @@ -0,0 +1,152 @@ +package haskell + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/go-test/deep" +) + +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{ + { + Name: "HTTP", + Version: "4000.3.16", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "HTTP", + Version: "4000.3.16", + PkgHash: fixtureP("6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71"), + SnapshotURL: &url, + }, + }, + { + Name: "configurator-pg", + Version: "0.2.6", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "configurator-pg", + Version: "0.2.6", + PkgHash: fixtureP("cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be"), + SnapshotURL: &url, + }, + }, + { + Name: "hasql-dynamic-statements", + Version: "0.3.1.1", + 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, + }, + }, + { + Name: "hasql-implicits", + Version: "0.1.0.4", + 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, + }, + }, + { + Name: "hasql-pool", + Version: "0.5.2.2", + 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, + }, + }, + { + Name: "lens-aeson", + Version: "1.1.3", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "lens-aeson", + Version: "1.1.3", + PkgHash: fixtureP("52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050"), + SnapshotURL: &url, + }, + }, + { + Name: "optparse-applicative", + Version: "0.16.1.0", + 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, + }, + }, + { + Name: "protolude", + Version: "0.3.2", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "protolude", + Version: "0.3.2", + PkgHash: fixtureP("2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1"), + SnapshotURL: &url, + }, + }, + { + Name: "ptr", + Version: "0.16.8.2", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "ptr", + Version: "0.16.8.2", + PkgHash: fixtureP("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) + } + + differences := deep.Equal(expected, actual) + if differences != nil { + t.Errorf("returned package list differed from expectation: %+v", differences) + } +} diff --git a/syft/pkg/cataloger/haskell/parse_stack_yaml.go b/syft/pkg/cataloger/haskell/parse_stack_yaml.go new file mode 100644 index 00000000000..eb71f51ca21 --- /dev/null +++ b/syft/pkg/cataloger/haskell/parse_stack_yaml.go @@ -0,0 +1,54 @@ +package haskell + +import ( + "fmt" + "io" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common" + "gopkg.in/yaml.v3" +) + +// integrity check +var _ common.ParserFn = 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) { + bytes, err := io.ReadAll(reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to load stack.yaml file: %w", err) + } + + var stackFile stackYaml + + if err := yaml.Unmarshal(bytes, &stackFile); err != nil { + return nil, nil, fmt.Errorf("failed to parse stack.yaml file: %w", err) + } + + 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, + }, + }) + } + + 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 new file mode 100644 index 00000000000..351630b7157 --- /dev/null +++ b/syft/pkg/cataloger/haskell/parse_stack_yaml_test.go @@ -0,0 +1,126 @@ +package haskell + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/go-test/deep" +) + +func TestParseStackYaml(t *testing.T) { + expected := []*pkg.Package{ + { + Name: "ShellCheck", + Version: "0.8.0", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "ShellCheck", + Version: "0.8.0", + PkgHash: fixtureP("353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df"), + }, + }, + { + Name: "colourista", + Version: "0.1.0.1", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "colourista", + Version: "0.1.0.1", + PkgHash: fixtureP("98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e"), + }, + }, + { + Name: "language-docker", + Version: "11.0.0", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "language-docker", + Version: "11.0.0", + PkgHash: fixtureP("3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55"), + }, + }, + { + Name: "spdx", + Version: "1.0.0.2", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "spdx", + Version: "1.0.0.2", + PkgHash: fixtureP("7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed"), + }, + }, + { + Name: "hspec", + Version: "2.9.4", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "hspec", + Version: "2.9.4", + PkgHash: fixtureP("658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3"), + }, + }, + { + Name: "hspec-core", + Version: "2.9.4", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "hspec-core", + Version: "2.9.4", + PkgHash: fixtureP("a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8"), + }, + }, + { + Name: "hspec-discover", + Version: "2.9.4", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "hspec-discover", + Version: "2.9.4", + PkgHash: fixtureP("fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6"), + }, + }, + { + Name: "stm", + Version: "2.5.0.2", + Language: pkg.Haskell, + Type: pkg.HackagePkg, + MetadataType: pkg.HackageMetadataType, + Metadata: pkg.HackageMetadata{ + Name: "stm", + Version: "2.5.0.2", + PkgHash: fixtureP("e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55"), + }, + }, + } + + fixture, err := os.Open("test-fixtures/stack.yaml") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + // TODO: no relationships are under test yet + actual, _, err := parseStackYaml(fixture.Name(), fixture) + if err != nil { + t.Error(err) + } + + differences := deep.Equal(expected, actual) + if differences != nil { + t.Errorf("returned package list differed from expectation: %+v", differences) + } +} diff --git a/syft/pkg/cataloger/haskell/test-fixtures/cabal.project.freeze b/syft/pkg/cataloger/haskell/test-fixtures/cabal.project.freeze new file mode 100644 index 00000000000..5c2cb9ac9bd --- /dev/null +++ b/syft/pkg/cataloger/haskell/test-fixtures/cabal.project.freeze @@ -0,0 +1,17 @@ +active-repositories: hackage.haskell.org:merge +constraints: any.Cabal ==3.2.1.0, + any.Diff ==0.4.1, + any.HTTP ==4000.3.16, + any.HUnit ==1.6.2.0, + any.OneTuple ==0.3.1, + tls +compat -hans +network, + any.Only ==0.1, + any.PyF ==0.10.2.0, + any.QuickCheck ==2.14.2, + semigroups +binary +bytestring -bytestring-builder +containers +deepseq +hashable +tagged +template-haskell +text +transformers +unordered-containers, + any.RSA ==2.4.1, + any.SHA ==1.6.4.4, + void -safe, + any.Spock ==0.14.0.0, + +index-state: hackage.haskell.org 2022-07-07T01:01:53Z diff --git a/syft/pkg/cataloger/haskell/test-fixtures/stack.yaml b/syft/pkg/cataloger/haskell/test-fixtures/stack.yaml new file mode 100644 index 00000000000..8c05906e1b0 --- /dev/null +++ b/syft/pkg/cataloger/haskell/test-fixtures/stack.yaml @@ -0,0 +1,16 @@ +flags: {} +extra-package-dbs: [] +packages: + - . +resolver: lts-18.28 +extra-deps: + - ShellCheck-0.8.0@sha256:353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df,3297 + - colourista-0.1.0.1@sha256:98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e,3307 + - language-docker-11.0.0@sha256:3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55,3883 + - spdx-1.0.0.2@sha256:7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed,2008 + - hspec-2.9.4@sha256:658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3,1709 + - hspec-core-2.9.4@sha256:a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8,6498 + - hspec-discover-2.9.4@sha256:fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6,2165 + - stm-2.5.0.2@sha256:e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55,2314 +ghc-options: + "$everything": -haddock diff --git a/syft/pkg/cataloger/haskell/test-fixtures/stack.yaml.lock b/syft/pkg/cataloger/haskell/test-fixtures/stack.yaml.lock new file mode 100644 index 00000000000..a185422556e --- /dev/null +++ b/syft/pkg/cataloger/haskell/test-fixtures/stack.yaml.lock @@ -0,0 +1,75 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: +- completed: + hackage: HTTP-4000.3.16@sha256:6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71,5947 + pantry-tree: + size: 1428 + sha256: b73a7f6d21cf20bbf819e19039409c9010efb5000d2b72cdd8fd67a9027c14e8 + original: + hackage: HTTP-4000.3.16@sha256:6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71,5947 +- completed: + hackage: configurator-pg-0.2.6@sha256:cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be,2849 + pantry-tree: + size: 2463 + sha256: 97efe7a22afc93033bda5adcffdabc0f1c30dc32b2c3ba02114ce7cd74c942fd + original: + hackage: configurator-pg-0.2.6@sha256:cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be,2849 +- completed: + hackage: hasql-dynamic-statements-0.3.1.1@sha256:2cfe6e75990e690f595a87cbe553f2e90fcd738610f6c66749c81cc4396b2cc4,2675 + pantry-tree: + size: 595 + sha256: b84ae10a5c776f88f546df73bc957a35e61056400b7e805dad0b254612907e97 + original: + hackage: hasql-dynamic-statements-0.3.1.1@sha256:2cfe6e75990e690f595a87cbe553f2e90fcd738610f6c66749c81cc4396b2cc4,2675 +- completed: + hackage: hasql-implicits-0.1.0.4@sha256:0848d3cbc9d94e1e539948fa0be4d0326b26335034161bf8076785293444ca6f,1361 + pantry-tree: + size: 264 + sha256: d49af8f8749ab7039fa668af4b78f997f7fa2928b4aded6798f573a3d08e76a0 + original: + hackage: hasql-implicits-0.1.0.4@sha256:0848d3cbc9d94e1e539948fa0be4d0326b26335034161bf8076785293444ca6f,1361 +- completed: + hackage: hasql-pool-0.5.2.2@sha256:b56d4dea112d97a2ef4b2749508c0ca646828cb2d77b827e8dc433d249bb2062,2438 + pantry-tree: + size: 412 + sha256: 2741a33f947d28b4076c798c20c1f646beecd21f5eaf522c8256cbeb34d4d6d0 + original: + hackage: hasql-pool-0.5.2.2@sha256:b56d4dea112d97a2ef4b2749508c0ca646828cb2d77b827e8dc433d249bb2062,2438 +- completed: + hackage: lens-aeson-1.1.3@sha256:52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050,1764 + pantry-tree: + size: 541 + sha256: b31392b78f2a03111c805f4400007778eb93b49f998ab41dfbebaaf9b5526bad + original: + hackage: lens-aeson-1.1.3@sha256:52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050,1764 +- completed: + hackage: optparse-applicative-0.16.1.0@sha256:418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731,4982 + pantry-tree: + size: 2979 + sha256: dd092d843091c08691485d68a1908517079b1bc6f3d73928f37635a19dc27fc1 + original: + hackage: optparse-applicative-0.16.1.0@sha256:418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731,4982 +- completed: + hackage: protolude-0.3.2@sha256:2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1,2240 + pantry-tree: + size: 1594 + sha256: a36d2912ac552d950ba4476de7d950b56b82dd28e48b9f4d0efee938f10bc525 + original: + hackage: protolude-0.3.2@sha256:2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1,2240 +- completed: + hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959 + pantry-tree: + size: 1303 + sha256: 557c438345de19f82bf01d676100da2a191ef06f624e7a4b90b09ac17cbb52a5 + original: + hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959 +snapshots: +- completed: + size: 618951 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml + sha256: 4c31d4ef975b0211078862566aedf3b82b6cea569fc2cde4c72a51e5a8d236ce + original: lts-19.14 diff --git a/syft/pkg/hackage_metadata.go b/syft/pkg/hackage_metadata.go new file mode 100644 index 00000000000..0f8c3801195 --- /dev/null +++ b/syft/pkg/hackage_metadata.go @@ -0,0 +1,28 @@ +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() +} diff --git a/syft/pkg/language.go b/syft/pkg/language.go index e4d3a1713f1..8e3d6193c6f 100644 --- a/syft/pkg/language.go +++ b/syft/pkg/language.go @@ -23,6 +23,7 @@ const ( Dotnet Language = "dotnet" Swift Language = "swift" CPP Language = "c++" + Haskell Language = "haskell" ) // AllLanguages is a set of all programming languages detected by syft. @@ -38,6 +39,7 @@ var AllLanguages = []Language{ Dotnet, Swift, CPP, + Haskell, } // String returns the string representation of the language. @@ -78,6 +80,8 @@ func LanguageByName(name string) Language { return Swift case packageurl.TypeConan, string(CPP): return CPP + case packageurl.TypeHackage, string(Haskell): + return Haskell default: return UnknownLanguage } diff --git a/syft/pkg/language_test.go b/syft/pkg/language_test.go index 69825928ce9..4acf8c37996 100644 --- a/syft/pkg/language_test.go +++ b/syft/pkg/language_test.go @@ -58,6 +58,10 @@ func TestLanguageFromPURL(t *testing.T) { purl: "pkg:conan/catch2@2.13.8", want: CPP, }, + { + purl: "pkg:hackage/HTTP@4000.3.16", + want: Haskell, + }, } var languages []string @@ -211,6 +215,14 @@ func TestLanguageByName(t *testing.T) { name: "c++", language: CPP, }, + { + name: "hackage", + language: Haskell, + }, + { + name: "haskell", + language: Haskell, + }, } for _, test := range tests { diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index 07591a16c15..b2c74a66261 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -28,6 +28,7 @@ const ( CocoapodsMetadataType MetadataType = "CocoapodsMetadataType" ConanaMetadataType MetadataType = "ConanaMetadataType" PortageMetadataType MetadataType = "PortageMetadata" + HackageMetadataType MetadataType = "HackageMetadataType" ) var AllMetadataTypes = []MetadataType{ @@ -48,6 +49,7 @@ var AllMetadataTypes = []MetadataType{ CocoapodsMetadataType, ConanaMetadataType, PortageMetadataType, + HackageMetadataType, } var MetadataTypeByName = map[MetadataType]reflect.Type{ @@ -68,4 +70,5 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{ CocoapodsMetadataType: reflect.TypeOf(CocoapodsMetadata{}), ConanaMetadataType: reflect.TypeOf(ConanMetadata{}), PortageMetadataType: reflect.TypeOf(PortageMetadata{}), + HackageMetadataType: reflect.TypeOf(HackageMetadata{}), } diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 8eddebf325c..6dafed4727d 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -26,6 +26,7 @@ const ( CocoapodsPkg Type = "pod" ConanPkg Type = "conan" PortagePkg Type = "portage" + HackagePkg Type = "hackage" ) // AllPkgs represents all supported package types @@ -48,6 +49,7 @@ var AllPkgs = []Type{ CocoapodsPkg, ConanPkg, PortagePkg, + HackagePkg, } // PackageURLType returns the PURL package type for the current package. @@ -85,6 +87,8 @@ func (t Type) PackageURLType() string { return packageurl.TypeConan case PortagePkg: return "portage" + case HackagePkg: + return packageurl.TypeHackage default: // TODO: should this be a "generic" purl type instead? return "" @@ -132,6 +136,8 @@ func TypeByName(name string) Type { return CocoapodsPkg case packageurl.TypeConan: return ConanPkg + case packageurl.TypeHackage: + return HackagePkg case "portage": return PortagePkg default: diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 3c806f5551c..2b25749ae51 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -76,6 +76,10 @@ func TestTypeFromPURL(t *testing.T) { purl: "pkg:conan/catch2@2.13.8", expected: ConanPkg, }, + { + purl: "pkg:hackage/HTTP@4000.3.16", + expected: HackagePkg, + }, } var pkgTypes []string diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index 7780db2ef30..757b05e2f1f 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -246,6 +246,21 @@ func TestPackageURL(t *testing.T) { }, expected: "pkg:conan/catch2@2.13.8", }, + { + 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 diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index ada610c65d2..512461105a2 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -281,6 +281,40 @@ var dirOnlyTestCases = []testCase{ "TinyConstraints": "4.0.2", }, }, + { + name: "find hackage packages", + pkgType: pkg.HackagePkg, + pkgLanguage: pkg.Haskell, + pkgInfo: map[string]string{ + "Cabal": "3.2.1.0", + "Diff": "0.4.1", + "HTTP": "4000.3.16", + "HUnit": "1.6.2.0", + "OneTuple": "0.3.1", + "Only": "0.1", + "PyF": "0.10.2.0", + "QuickCheck": "2.14.2", + "RSA": "2.4.1", + "SHA": "1.6.4.4", + "Spock": "0.14.0.0", + "ShellCheck": "0.8.0", + "colourista": "0.1.0.1", + "language-docker": "11.0.0", + "spdx": "1.0.0.2", + "hspec": "2.9.4", + "hspec-core": "2.9.4", + "hspec-discover": "2.9.4", + "stm": "2.5.0.2", + "configurator-pg": "0.2.6", + "hasql-dynamic-statements": "0.3.1.1", + "hasql-implicits": "0.1.0.4", + "hasql-pool": "0.5.2.2", + "lens-aeson": "1.1.3", + "optparse-applicative": "0.16.1.0", + "protolude": "0.3.2", + "ptr": "0.16.8.2", + }, + }, } var commonTestCases = []testCase{ diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index ce909a733a6..9cd047fc27e 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -69,6 +69,7 @@ func TestPkgCoverageImage(t *testing.T) { definedLanguages.Remove(pkg.Dotnet.String()) definedLanguages.Remove(string(pkg.Swift.String())) definedLanguages.Remove(pkg.CPP.String()) + definedLanguages.Remove(pkg.Haskell.String()) observedPkgs := internal.NewStringSet() definedPkgs := internal.NewStringSet() @@ -84,6 +85,7 @@ func TestPkgCoverageImage(t *testing.T) { definedPkgs.Remove(string(pkg.DotnetPkg)) definedPkgs.Remove(string(pkg.CocoapodsPkg)) definedPkgs.Remove(string(pkg.ConanPkg)) + definedPkgs.Remove(string(pkg.HackagePkg)) var cases []testCase cases = append(cases, commonTestCases...) diff --git a/test/integration/test-fixtures/image-pkg-coverage/hackage/cabal.project.freeze b/test/integration/test-fixtures/image-pkg-coverage/hackage/cabal.project.freeze new file mode 100644 index 00000000000..f206bbb4671 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/hackage/cabal.project.freeze @@ -0,0 +1,16 @@ +active-repositories: hackage.haskell.org:merge +constraints: any.Cabal ==3.2.1.0, + any.Diff ==0.4.1, + any.HUnit ==1.6.2.0, + any.OneTuple ==0.3.1, + tls +compat -hans +network, + any.Only ==0.1, + any.PyF ==0.10.2.0, + any.QuickCheck ==2.14.2, + semigroups +binary +bytestring -bytestring-builder +containers +deepseq +hashable +tagged +template-haskell +text +transformers +unordered-containers, + any.RSA ==2.4.1, + any.SHA ==1.6.4.4, + void -safe, + any.Spock ==0.14.0.0, + +index-state: hackage.haskell.org 2022-07-07T01:01:53Z diff --git a/test/integration/test-fixtures/image-pkg-coverage/hackage/stack.yaml b/test/integration/test-fixtures/image-pkg-coverage/hackage/stack.yaml new file mode 100644 index 00000000000..8c05906e1b0 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/hackage/stack.yaml @@ -0,0 +1,16 @@ +flags: {} +extra-package-dbs: [] +packages: + - . +resolver: lts-18.28 +extra-deps: + - ShellCheck-0.8.0@sha256:353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df,3297 + - colourista-0.1.0.1@sha256:98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e,3307 + - language-docker-11.0.0@sha256:3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55,3883 + - spdx-1.0.0.2@sha256:7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed,2008 + - hspec-2.9.4@sha256:658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3,1709 + - hspec-core-2.9.4@sha256:a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8,6498 + - hspec-discover-2.9.4@sha256:fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6,2165 + - stm-2.5.0.2@sha256:e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55,2314 +ghc-options: + "$everything": -haddock diff --git a/test/integration/test-fixtures/image-pkg-coverage/hackage/stack.yaml.lock b/test/integration/test-fixtures/image-pkg-coverage/hackage/stack.yaml.lock new file mode 100644 index 00000000000..a185422556e --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/hackage/stack.yaml.lock @@ -0,0 +1,75 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: +- completed: + hackage: HTTP-4000.3.16@sha256:6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71,5947 + pantry-tree: + size: 1428 + sha256: b73a7f6d21cf20bbf819e19039409c9010efb5000d2b72cdd8fd67a9027c14e8 + original: + hackage: HTTP-4000.3.16@sha256:6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71,5947 +- completed: + hackage: configurator-pg-0.2.6@sha256:cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be,2849 + pantry-tree: + size: 2463 + sha256: 97efe7a22afc93033bda5adcffdabc0f1c30dc32b2c3ba02114ce7cd74c942fd + original: + hackage: configurator-pg-0.2.6@sha256:cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be,2849 +- completed: + hackage: hasql-dynamic-statements-0.3.1.1@sha256:2cfe6e75990e690f595a87cbe553f2e90fcd738610f6c66749c81cc4396b2cc4,2675 + pantry-tree: + size: 595 + sha256: b84ae10a5c776f88f546df73bc957a35e61056400b7e805dad0b254612907e97 + original: + hackage: hasql-dynamic-statements-0.3.1.1@sha256:2cfe6e75990e690f595a87cbe553f2e90fcd738610f6c66749c81cc4396b2cc4,2675 +- completed: + hackage: hasql-implicits-0.1.0.4@sha256:0848d3cbc9d94e1e539948fa0be4d0326b26335034161bf8076785293444ca6f,1361 + pantry-tree: + size: 264 + sha256: d49af8f8749ab7039fa668af4b78f997f7fa2928b4aded6798f573a3d08e76a0 + original: + hackage: hasql-implicits-0.1.0.4@sha256:0848d3cbc9d94e1e539948fa0be4d0326b26335034161bf8076785293444ca6f,1361 +- completed: + hackage: hasql-pool-0.5.2.2@sha256:b56d4dea112d97a2ef4b2749508c0ca646828cb2d77b827e8dc433d249bb2062,2438 + pantry-tree: + size: 412 + sha256: 2741a33f947d28b4076c798c20c1f646beecd21f5eaf522c8256cbeb34d4d6d0 + original: + hackage: hasql-pool-0.5.2.2@sha256:b56d4dea112d97a2ef4b2749508c0ca646828cb2d77b827e8dc433d249bb2062,2438 +- completed: + hackage: lens-aeson-1.1.3@sha256:52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050,1764 + pantry-tree: + size: 541 + sha256: b31392b78f2a03111c805f4400007778eb93b49f998ab41dfbebaaf9b5526bad + original: + hackage: lens-aeson-1.1.3@sha256:52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050,1764 +- completed: + hackage: optparse-applicative-0.16.1.0@sha256:418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731,4982 + pantry-tree: + size: 2979 + sha256: dd092d843091c08691485d68a1908517079b1bc6f3d73928f37635a19dc27fc1 + original: + hackage: optparse-applicative-0.16.1.0@sha256:418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731,4982 +- completed: + hackage: protolude-0.3.2@sha256:2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1,2240 + pantry-tree: + size: 1594 + sha256: a36d2912ac552d950ba4476de7d950b56b82dd28e48b9f4d0efee938f10bc525 + original: + hackage: protolude-0.3.2@sha256:2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1,2240 +- completed: + hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959 + pantry-tree: + size: 1303 + sha256: 557c438345de19f82bf01d676100da2a191ef06f624e7a4b90b09ac17cbb52a5 + original: + hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959 +snapshots: +- completed: + size: 618951 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml + sha256: 4c31d4ef975b0211078862566aedf3b82b6cea569fc2cde4c72a51e5a8d236ce + original: lts-19.14