Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement haskell support #1096

Merged
merged 1 commit into from Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions internal/formats/common/spdxhelpers/source_info.go
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)

//nolint:funlen
func SourceInfo(p pkg.Package) string {
answer := ""
switch p.Type {
Expand Down Expand Up @@ -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"
}
Expand Down
8 changes: 8 additions & 0 deletions internal/formats/common/spdxhelpers/source_info_test.go
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions internal/formats/syftjson/model/package.go
Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions syft/pkg/cataloger/cataloger.go
Expand Up @@ -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"
Expand Down Expand Up @@ -82,6 +83,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger {
swift.NewCocoapodsCataloger(),
cpp.NewConanfileCataloger(),
portage.NewPortageCataloger(),
haskell.NewHackageCataloger(),
}, cfg.Catalogers)
}

Expand Down Expand Up @@ -110,6 +112,7 @@ func AllCatalogers(cfg Config) []Cataloger {
swift.NewCocoapodsCataloger(),
cpp.NewConanfileCataloger(),
portage.NewPortageCataloger(),
haskell.NewHackageCataloger(),
}, cfg.Catalogers)
}

Expand Down
15 changes: 15 additions & 0 deletions 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")
}
53 changes: 53 additions & 0 deletions 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,
},
})
}
}
151 changes: 151 additions & 0 deletions 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)
}
}
90 changes: 90 additions & 0 deletions 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
}