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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fetch Dart package versions from sdk entries #3572

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion syft/pkg/cataloger/dart/parse_pubspec_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"fmt"
"net/url"
"regexp"
"sort"

"github.com/Masterminds/semver"
"gopkg.in/yaml.v3"

"github.com/anchore/syft/internal/log"
Expand Down Expand Up @@ -68,7 +70,26 @@ func parsePubspecLock(_ context.Context, _ file.Resolver, _ *generic.Environment
}

var names []string
for name := range p.Packages {
for name, pkg := range p.Packages {
if pkg.Source == "sdk" && pkg.Version == "0.0.0" {
// Packages that are delivered as part of an SDK (e.g. Flutter) have their
// version set to "0.0.0" in the package definition. The actual version
// should refer to the SDK version, which is defined in a dedicated section
// in the pubspec.lock file and uses a version range constraint.
//
// If such a package is detected, look up the version range constraint of
// its matching SDK, and set the minimum supported version as its new version.
sdkName := pkg.Description.Name
sdkVersion, err := p.getSdkVersion(sdkName)

if err != nil {
wagoodman marked this conversation as resolved.
Show resolved Hide resolved
log.Tracef("failed to resolve %s SDK version for package %s: %v", sdkName, name, err)
continue
}
pkg.Version = sdkVersion
p.Packages[name] = pkg
}

names = append(names, name)
}

Expand All @@ -89,6 +110,68 @@ func parsePubspecLock(_ context.Context, _ file.Resolver, _ *generic.Environment
return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages")
}

// Look up the version range constraint for a given sdk name, if found,
// and return its lowest supported version matching that constraint.
//
// The sdks and their constraints are defined in the pubspec.lock file, e.g.
//
// sdks:
// dart: ">=2.12.0 <3.0.0"
// flutter: ">=3.24.5"
//
// and stored in the pubspecLock.Sdks map during parsing.
//
// Example based on the data above:
//
// getSdkVersion("dart") -> "2.12.0"
// getSdkVersion("flutter") -> "3.24.5"
// getSdkVersion("undefined") -> error
func (psl *pubspecLock) getSdkVersion(sdk string) (string, error) {
constraint, found := psl.Sdks[sdk]

if !found {
return "", fmt.Errorf("cannot find %s SDK", sdk)
}

return parseMinimumSdkVersion(constraint)
wagoodman marked this conversation as resolved.
Show resolved Hide resolved
}

// Parse a given version range constraint and return its lowest supported version.
//
// This is intended for packages that are part of an SDK (e.g. Flutter) and don't
// have an explicit version string set. This will take the given constraint
// parameter, ensure it's a valid constraint string, and return the lowest version
// within that constraint range.
//
// Examples:
//
// parseMinimumSdkVersion("^1.2.3") -> "1.2.3"
// parseMinimumSdkVersion(">=1.2.3") -> "1.2.3"
// parseMinimumSdkVersion(">=1.2.3 <2.0.0") -> "1.2.3"
// parseMinimumSdkVersion("1.2.3") -> error
//
// see https://dart.dev/tools/pub/dependencies#version-constraints for the
// constraint format used in Dart SDK defintions.
func parseMinimumSdkVersion(constraint string) (string, error) {
// Match strings that
// 1. start with either "^" or ">=" (Dart SDK constraints only use those two)
// 2. followed by a valid semantic version, matched as "version" named subexpression
// 3. followed by a space (if there's a range) or end of string (if there's only a lower boundary)
// |---1--||------------------2------------------||-3-|
re := regexp.MustCompile(`^(\^|>=)(?P<version>` + semver.SemVerRegex + `)( |$)`)

if !re.MatchString(constraint) {
return "", fmt.Errorf("unsupported or invalid constraint '%s'", constraint)
}

// Read "version" subexpression (see 2. above) into version variable
var version []byte
matchIndex := re.FindStringSubmatchIndex(constraint)
version = re.ExpandString(version, "$version", constraint, matchIndex)

return string(version), nil
}

func (p *pubspecLockPackage) getVcsURL() string {
if p.Source == "git" {
if p.Description.Path == "." {
Expand Down
168 changes: 165 additions & 3 deletions syft/pkg/cataloger/dart/parse_pubspec_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package dart
import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
Expand Down Expand Up @@ -76,14 +78,14 @@ func TestParsePubspecLock(t *testing.T) {
},
{
Name: "flutter",
Version: "0.0.0",
PURL: "pkg:pub/flutter@0.0.0",
Version: "3.24.5",
PURL: "pkg:pub/flutter@3.24.5",
Locations: fixtureLocationSet,
Language: pkg.Dart,
Type: pkg.DartPubPkg,
Metadata: pkg.DartPubspecLockEntry{
Name: "flutter",
Version: "0.0.0",
Version: "3.24.5",
},
},
{
Expand Down Expand Up @@ -113,3 +115,163 @@ func Test_corruptPubspecLock(t *testing.T) {
WithError().
TestParser(t, parsePubspecLock)
}

func Test_missingSdkEntryPubspecLock(t *testing.T) {
fixture := "test-fixtures/missing-sdk/pubspec.lock"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))

// SDK version is missing, so flutter version cannot be determined and
// is ignored, expecting args as only package in the list as a result.
expected := []pkg.Package{
{
Name: "args",
Version: "1.6.0",
PURL: "pkg:pub/[email protected]",
Locations: fixtureLocationSet,
Language: pkg.Dart,
Type: pkg.DartPubPkg,
Metadata: pkg.DartPubspecLockEntry{
Name: "args",
Version: "1.6.0",
},
},
}

// TODO: relationships are not under test
var expectedRelationships []artifact.Relationship

pkgtest.TestFileParser(t, fixture, parsePubspecLock, expected, expectedRelationships)
}

func Test_invalidSdkEntryPubspecLock(t *testing.T) {
fixture := "test-fixtures/invalid-sdk/pubspec.lock"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))

// SDK version is invalid, so flutter version cannot be determined and
// is ignored, expecting args as only package in the list as a result.
expected := []pkg.Package{
{
Name: "args",
Version: "1.6.0",
PURL: "pkg:pub/[email protected]",
Locations: fixtureLocationSet,
Language: pkg.Dart,
Type: pkg.DartPubPkg,
Metadata: pkg.DartPubspecLockEntry{
Name: "args",
Version: "1.6.0",
},
},
}

// TODO: relationships are not under test
var expectedRelationships []artifact.Relationship

pkgtest.TestFileParser(t, fixture, parsePubspecLock, expected, expectedRelationships)
}

func Test_sdkVersionLookup(t *testing.T) {
psl := &pubspecLock{
Sdks: make(map[string]string, 5),
}

psl.Sdks["minVersionSdk"] = ">=0.1.2"
psl.Sdks["rangeVersionSdk"] = ">=1.2.3 <2.0.0"
psl.Sdks["caretVersionSdk"] = "^2.3.4"
psl.Sdks["emptyVersionSdk"] = ""
psl.Sdks["invalidVersionSdk"] = "not a constraint"

var version string
var err error

version, err = psl.getSdkVersion("minVersionSdk")
assert.NoError(t, err)
assert.Equal(t, "0.1.2", version)

version, err = psl.getSdkVersion("rangeVersionSdk")
assert.NoError(t, err)
assert.Equal(t, "1.2.3", version)

version, err = psl.getSdkVersion("caretVersionSdk")
assert.NoError(t, err)
assert.Equal(t, "2.3.4", version)

version, err = psl.getSdkVersion("emptyVersionSdk")
assert.Error(t, err)
assert.Equal(t, "", version)

version, err = psl.getSdkVersion("invalidVersionSdk")
assert.Error(t, err)
assert.Equal(t, "", version)

version, err = psl.getSdkVersion("nonexistantSdk")
assert.Error(t, err)
assert.Equal(t, "", version)
}

func Test_sdkVersionParser_valid(t *testing.T) {
var version string
var err error

// map constraints to expected version
patterns := map[string]string{
"^0.0.0": "0.0.0",
">=0.0.0": "0.0.0",
"^1.23.4": "1.23.4",
">=1.23.4": "1.23.4",
"^11.22.33": "11.22.33",
">=11.22.33": "11.22.33",
"^123.123456.12345678": "123.123456.12345678",
">=123.123456.12345678": "123.123456.12345678",
">=1.2.3 <2.3.4": "1.2.3",
">=1.2.3 random string": "1.2.3",
">=1.2.3 >=0.1.2": "1.2.3",
"^1.2": "1.2",
">=1.2": "1.2",
"^1.2.3-rc4": "1.2.3-rc4",
">=1.2.3-rc4": "1.2.3-rc4",
"^2.34.5+hotfix6": "2.34.5+hotfix6",
">=2.34.5+hotfix6": "2.34.5+hotfix6",
}

for constraint, expected := range patterns {
version, err = parseMinimumSdkVersion(constraint)
assert.NoError(t, err)
assert.Equalf(t, expected, version, "constraint '%s", constraint)
}
}

func Test_sdkVersionParser_invalid(t *testing.T) {
var version string
var err error

patterns := []string{
"",
"abc",
"^abc",
">=abc",
"^a.b.c",
">=a.b.c",
"1.2.34",
">1.2.34",
"<=1.2.34",
"<1.2.34",
"^1.2.3.4",
">=1.2.3.4",
"^1.x.0",
">=1.x.0",
"^1x2x3",
">=1x2x3",
"^1.-2.3",
">=1.-2.3",
"abc <1.2.34",
"^2.3.45hotfix6",
">=2.3.45hotfix6",
}

for _, pattern := range patterns {
version, err = parseMinimumSdkVersion(pattern)
assert.Error(t, err)
assert.Equalf(t, "", version, "constraint '%s'", pattern)
}
}
15 changes: 15 additions & 0 deletions syft/pkg/cataloger/dart/test-fixtures/invalid-sdk/pubspec.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
sdks:
flutter: "3.24.5"
13 changes: 13 additions & 0 deletions syft/pkg/cataloger/dart/test-fixtures/missing-sdk/pubspec.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
1 change: 1 addition & 0 deletions syft/pkg/cataloger/dart/test-fixtures/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ packages:
version: "1.11.20"
sdks:
dart: ">=2.12.0 <3.0.0"
flutter: ">=3.24.5"
Loading