From cb3e73e3089f171518b3f4928f76313109633ce8 Mon Sep 17 00:00:00 2001 From: Eric Larssen Date: Thu, 31 Mar 2022 14:44:55 -0500 Subject: [PATCH] Add dart support (#919) Co-authored-by: Alex Goodman --- README.md | 1 + internal/constants.go | 2 +- .../formats/common/spdxhelpers/source_info.go | 2 + .../common/spdxhelpers/source_info_test.go | 8 + internal/formats/syftjson/model/package.go | 6 + .../snapshot/TestDirectoryEncoder.golden | 4 +- .../TestEncodeFullJSONDocument.golden | 4 +- .../snapshot/TestImageEncoder.golden | 4 +- schema/json/generate.go | 1 + schema/json/schema-3.2.2.json | 1261 +++++++++++++++++ syft/pkg/cataloger/cataloger.go | 3 + syft/pkg/cataloger/dart/cataloger.go | 14 + syft/pkg/cataloger/dart/parse_pubspec_lock.go | 96 ++ .../cataloger/dart/parse_pubspec_lock_test.go | 99 ++ .../cataloger/dart/test-fixtures/pubspec.lock | 49 + syft/pkg/dart_pub_metadata.go | 38 + syft/pkg/language.go | 4 + syft/pkg/language_test.go | 7 +- syft/pkg/metadata.go | 3 + syft/pkg/type.go | 6 + syft/pkg/type_test.go | 7 +- syft/pkg/url_test.go | 14 + .../catalog_packages_cases_test.go | 13 + test/integration/catalog_packages_test.go | 5 +- .../image-pkg-coverage/pkgs/dart/pubspec.lock | 49 + 25 files changed, 1690 insertions(+), 10 deletions(-) create mode 100644 schema/json/schema-3.2.2.json create mode 100644 syft/pkg/cataloger/dart/cataloger.go create mode 100644 syft/pkg/cataloger/dart/parse_pubspec_lock.go create mode 100644 syft/pkg/cataloger/dart/parse_pubspec_lock_test.go create mode 100644 syft/pkg/cataloger/dart/test-fixtures/pubspec.lock create mode 100644 syft/pkg/dart_pub_metadata.go create mode 100644 test/integration/test-fixtures/image-pkg-coverage/pkgs/dart/pubspec.lock diff --git a/README.md b/README.md index 905efec8d..ab39de4b1 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A CLI tool and Go library for generating a Software Bill of Materials (SBOM) fro ### Supported Ecosystems - Alpine (apk) +- Dart (pubs) - Debian (dpkg) - Go (go.mod, Go binaries) - Java (jar, ear, war, par, sar) diff --git a/internal/constants.go b/internal/constants.go index 3d2266a00..da098e089 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -6,5 +6,5 @@ const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "3.2.1" + JSONSchemaVersion = "3.2.2" ) diff --git a/internal/formats/common/spdxhelpers/source_info.go b/internal/formats/common/spdxhelpers/source_info.go index d0ae4ed80..ed5e2600b 100644 --- a/internal/formats/common/spdxhelpers/source_info.go +++ b/internal/formats/common/spdxhelpers/source_info.go @@ -13,6 +13,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from RPM DB" case pkg.ApkPkg: answer = "acquired package info from APK DB" + case pkg.DartPubPkg: + answer = "acquired package info from pubspec manifest" case pkg.DebPkg: answer = "acquired package info from DPKG DB" case pkg.NpmPkg: diff --git a/internal/formats/common/spdxhelpers/source_info_test.go b/internal/formats/common/spdxhelpers/source_info_test.go index 05e99d1bf..56f6c5bbd 100644 --- a/internal/formats/common/spdxhelpers/source_info_test.go +++ b/internal/formats/common/spdxhelpers/source_info_test.go @@ -126,6 +126,14 @@ func Test_SourceInfo(t *testing.T) { "from PHP composer manifest", }, }, + { + input: pkg.Package{ + Type: pkg.DartPubPkg, + }, + expected: []string{ + "from pubspec manifest", + }, + }, } 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 618e703cb..9ae55ee51 100644 --- a/internal/formats/syftjson/model/package.go +++ b/internal/formats/syftjson/model/package.go @@ -130,6 +130,12 @@ func (p *Package) UnmarshalJSON(b []byte) error { return err } p.Metadata = payload + case pkg.DartPubMetadataType: + var payload pkg.DartPubMetadata + 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/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 05c55483e..92b55b015 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -88,7 +88,7 @@ } }, "schema": { - "version": "3.2.1", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.1.json" + "version": "3.2.2", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.2.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index e600234ae..728667abd 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -184,7 +184,7 @@ } }, "schema": { - "version": "3.2.1", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.1.json" + "version": "3.2.2", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.2.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index 50a3f6ced..baa8ed26a 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -111,7 +111,7 @@ } }, "schema": { - "version": "3.2.1", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.1.json" + "version": "3.2.2", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.2.json" } } diff --git a/schema/json/generate.go b/schema/json/generate.go index 140ccd3e7..a2ff68e57 100644 --- a/schema/json/generate.go +++ b/schema/json/generate.go @@ -37,6 +37,7 @@ type artifactMetadataContainer struct { Cargo pkg.CargoPackageMetadata Go pkg.GolangBinMetadata Php pkg.PhpComposerJSONMetadata + Dart pkg.DartPubMetadata } func main() { diff --git a/schema/json/schema-3.2.2.json b/schema/json/schema-3.2.2.json new file mode 100644 index 000000000..d6649f6cf --- /dev/null +++ b/schema/json/schema-3.2.2.json @@ -0,0 +1,1261 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Document", + "definitions": { + "ApkFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + } + }, + "additionalProperties": true, + "type": "object" + }, + "ApkMetadata": { + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "license", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "type": "string" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ApkFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "CargoPackageMetadata": { + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Classification": { + "required": [ + "class", + "metadata" + ], + "properties": { + "class": { + "type": "string" + }, + "metadata": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Coordinates": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DartPubMetadata": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Descriptor": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "Digest": { + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Document": { + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "properties": { + "artifacts": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Secrets" + }, + "type": "array" + }, + "source": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Source" + }, + "distro": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/LinuxRelease" + }, + "descriptor": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Descriptor" + }, + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Schema" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgFileRecord": { + "required": [ + "path", + "isConfigFile" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgMetadata": { + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/DpkgFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "File": { + "required": [ + "id", + "location" + ], + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/Coordinates" + }, + "metadata": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + }, + "type": "array" + }, + "classifications": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Classification" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "FileMetadataEntry": { + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType" + ], + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "GemMetadata": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "GolangBinMetadata": { + "required": [ + "goCompiledVersion", + "architecture" + ], + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "JavaMetadata": { + "required": [ + "virtualPath" + ], + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/JavaManifest" + }, + "pomProperties": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomProperties" + }, + "pomProject": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomProject" + } + }, + "additionalProperties": true, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "items": { + "type": "string" + }, + "type": "array" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "NpmPackageJSONMetadata": { + "required": [ + "name", + "version", + "author", + "licenses", + "homepage", + "description", + "url" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "author": { + "type": "string" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Package": { + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Coordinates" + }, + "type": "array" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/ApkMetadata" + }, + { + "$ref": "#/definitions/CargoPackageMetadata" + }, + { + "$ref": "#/definitions/DartPubMetadata" + }, + { + "$ref": "#/definitions/DpkgMetadata" + }, + { + "$ref": "#/definitions/GemMetadata" + }, + { + "$ref": "#/definitions/GolangBinMetadata" + }, + { + "$ref": "#/definitions/JavaMetadata" + }, + { + "$ref": "#/definitions/NpmPackageJSONMetadata" + }, + { + "$ref": "#/definitions/PhpComposerJSONMetadata" + }, + { + "$ref": "#/definitions/PythonPackageMetadata" + }, + { + "$ref": "#/definitions/RpmdbMetadata" + } + ] + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerAuthors": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerExternalReference": { + "required": [ + "type", + "url", + "reference" + ], + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerJSONMetadata": { + "required": [ + "name", + "version", + "source", + "dist" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/definitions/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomParent": { + "required": [ + "groupId", + "artifactId", + "version" + ], + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomProject": { + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomProperties": { + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version", + "extraFields" + ], + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonDirectURLOriginInfo": { + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonFileDigest": { + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonPackageMetadata": { + "required": [ + "name", + "version", + "license", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonDirectURLOriginInfo" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Relationship": { + "required": [ + "parent", + "child", + "type" + ], + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbFileRecord": { + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbMetadata": { + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "license", + "vendor", + "files" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "license": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/RpmdbFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Schema": { + "required": [ + "version", + "url" + ], + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "SearchResult": { + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ], + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Secrets": { + "required": [ + "location", + "secrets" + ], + "properties": { + "location": { + "$ref": "#/definitions/Coordinates" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/SearchResult" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Source": { + "required": [ + "type", + "target" + ], + "properties": { + "type": { + "type": "string" + }, + "target": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + } + } +} diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index f44efeb4b..8e10bf5d6 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/apkdb" + "github.com/anchore/syft/syft/pkg/cataloger/dart" "github.com/anchore/syft/syft/pkg/cataloger/deb" "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/java" @@ -61,6 +62,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger { golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), rust.NewCargoLockCataloger(), + dart.NewPubspecLockCataloger(), } } @@ -80,5 +82,6 @@ func AllCatalogers(cfg Config) []Cataloger { golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), rust.NewCargoLockCataloger(), + dart.NewPubspecLockCataloger(), } } diff --git a/syft/pkg/cataloger/dart/cataloger.go b/syft/pkg/cataloger/dart/cataloger.go new file mode 100644 index 000000000..30fd9203f --- /dev/null +++ b/syft/pkg/cataloger/dart/cataloger.go @@ -0,0 +1,14 @@ +package dart + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/common" +) + +// NewPubspecLockCataloger returns a new Dartlang cataloger object base on pubspec lock files. +func NewPubspecLockCataloger() *common.GenericCataloger { + globParsers := map[string]common.ParserFn{ + "**/pubspec.lock": parsePubspecLock, + } + + return common.NewGenericCataloger(nil, globParsers, "dartlang-lock-cataloger") +} diff --git a/syft/pkg/cataloger/dart/parse_pubspec_lock.go b/syft/pkg/cataloger/dart/parse_pubspec_lock.go new file mode 100644 index 000000000..2ecd8177c --- /dev/null +++ b/syft/pkg/cataloger/dart/parse_pubspec_lock.go @@ -0,0 +1,96 @@ +package dart + +import ( + "fmt" + "io" + "net/url" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common" + "gopkg.in/yaml.v2" +) + +// integrity check +var _ common.ParserFn = parsePubspecLock + +const defaultPubRegistry string = "https://pub.dartlang.org" + +type pubspecLock struct { + Packages map[string]pubspecLockPackage `yaml:"packages"` + Sdks map[string]string `yaml:"sdks"` +} + +type pubspecLockPackage struct { + Dependency string `yaml:"dependency" mapstructure:"dependency"` + Description pubspecLockDescription `yaml:"description" mapstructure:"description"` + Source string `yaml:"source" mapstructure:"source"` + Version string `yaml:"version" mapstructure:"version"` +} + +type pubspecLockDescription struct { + Name string `yaml:"name" mapstructure:"name"` + URL string `yaml:"url" mapstructure:"url"` + Path string `yaml:"path" mapstructure:"path"` + Ref string `yaml:"ref" mapstructure:"ref"` + ResolvedRef string `yaml:"resolved-ref" mapstructure:"resolved-ref"` +} + +func parsePubspecLock(path string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { + var packages []*pkg.Package + + dec := yaml.NewDecoder(reader) + + var p pubspecLock + if err := dec.Decode(&p); err != nil { + return nil, nil, fmt.Errorf("failed to parse pubspec.lock file: %w", err) + } + + for name, pubPkg := range p.Packages { + packages = append(packages, newPubspecLockPackage(name, pubPkg)) + } + + return packages, nil, nil +} + +func newPubspecLockPackage(name string, p pubspecLockPackage) *pkg.Package { + return &pkg.Package{ + Name: name, + Version: p.Version, + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: &pkg.DartPubMetadata{ + Name: name, + Version: p.Version, + HostedURL: p.getHostedURL(), + VcsURL: p.getVcsURL(), + }, + } +} + +func (p *pubspecLockPackage) getVcsURL() string { + if p.Source == "git" { + if p.Description.Path == "." { + return fmt.Sprintf("%s@%s", p.Description.URL, p.Description.ResolvedRef) + } + + return fmt.Sprintf("%s@%s#%s", p.Description.URL, p.Description.ResolvedRef, p.Description.Path) + } + + return "" +} + +func (p *pubspecLockPackage) getHostedURL() string { + if p.Source == "hosted" && p.Description.URL != defaultPubRegistry { + u, err := url.Parse(p.Description.URL) + if err != nil { + log.Debugf("Unable to parse registry url %w", err) + return p.Description.URL + } + return u.Host + } + + return "" +} diff --git a/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go b/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go new file mode 100644 index 000000000..f881524c8 --- /dev/null +++ b/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go @@ -0,0 +1,99 @@ +package dart + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" +) + +func assertPackagesEqual(t *testing.T, actual []*pkg.Package, expected map[string]*pkg.Package) { + assert.Len(t, actual, len(expected)) +} + +func TestParsePubspecLock(t *testing.T) { + expected := map[string]*pkg.Package{ + "ale": { + Name: "ale", + Version: "3.3.0", + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "ale", + Version: "3.3.0", + HostedURL: "pub.hosted.org", + }, + }, + "analyzer": { + Name: "analyzer", + Version: "0.40.7", + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "analyzer", + Version: "0.40.7", + }, + }, + "ansicolor": { + Name: "ansicolor", + Version: "1.1.1", + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "ansicolor", + Version: "1.1.1", + }, + }, + "archive": { + Name: "archive", + Version: "2.0.13", + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "archive", + Version: "2.0.13", + }, + }, + "args": { + Name: "args", + Version: "1.6.0", + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "args", + Version: "1.6.0", + }, + }, + "key_binder": { + Name: "key_binder", + Version: "1.11.20", + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "key_binder", + Version: "1.11.20", + VcsURL: "git@github.com:Workiva/key_binder.git#3f7b3a6350e73c7dcac45301c0e18fbd42af02f7", + }, + }, + } + + fixture, err := os.Open("test-fixtures/pubspec.lock") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + actual, _, err := parsePubspecLock(fixture.Name(), fixture) + if err != nil { + t.Fatalf("failed to parse pubspec.lock: %+v", err) + } + + assertPackagesEqual(t, actual, expected) +} diff --git a/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock b/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock new file mode 100644 index 000000000..c7b912f1a --- /dev/null +++ b/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock @@ -0,0 +1,49 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + ale: + dependency: transitive + description: + name: ale + url: "https://pub.hosted.org" + source: hosted + version: "3.3.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.40.7" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + key_binder: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "3f7b3a6350e73c7dcac45301c0e18fbd42af02f7" + url: "git@github.com:Workiva/key_binder.git" + source: git + version: "1.11.20" +sdks: + dart: ">=2.12.0 <3.0.0" diff --git a/syft/pkg/dart_pub_metadata.go b/syft/pkg/dart_pub_metadata.go new file mode 100644 index 000000000..3c800877b --- /dev/null +++ b/syft/pkg/dart_pub_metadata.go @@ -0,0 +1,38 @@ +package pkg + +import ( + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/linux" +) + +type DartPubMetadata struct { + Name string `mapstructure:"name" json:"name"` + Version string `mapstructure:"version" json:"version"` + HostedURL string `mapstructure:"hosted_url" json:"hosted_url,omitempty"` + VcsURL string `mapstructure:"vcs_url" json:"vcs_url,omitempty"` +} + +func (m DartPubMetadata) PackageURL(_ *linux.Release) string { + var qualifiers packageurl.Qualifiers + + if m.HostedURL != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "hosted_url", + Value: m.HostedURL, + }) + } else if m.VcsURL != "" { // Default to using Hosted if somehow both are provided + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "vcs_url", + Value: m.VcsURL, + }) + } + + return packageurl.NewPackageURL( + packageurl.TypePub, + "", + m.Name, + m.Version, + qualifiers, + "", + ).ToString() +} diff --git a/syft/pkg/language.go b/syft/pkg/language.go index 42e96b7ab..b4b3734a3 100644 --- a/syft/pkg/language.go +++ b/syft/pkg/language.go @@ -19,6 +19,7 @@ const ( Ruby Language = "ruby" Go Language = "go" Rust Language = "rust" + Dart Language = "dart" ) // AllLanguages is a set of all programming languages detected by syft. @@ -30,6 +31,7 @@ var AllLanguages = []Language{ Ruby, Go, Rust, + Dart, } // String returns the string representation of the language. @@ -62,6 +64,8 @@ func LanguageByName(name string) Language { return Ruby case purlCargoPkgType: return Rust + case packageurl.TypePub, string(Dart): + return Dart default: return UnknownLanguage } diff --git a/syft/pkg/language_test.go b/syft/pkg/language_test.go index f2989c37a..2e1d5bc9a 100644 --- a/syft/pkg/language_test.go +++ b/syft/pkg/language_test.go @@ -1,9 +1,10 @@ package pkg import ( + "testing" + "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" - "testing" ) func TestLanguageFromPURL(t *testing.T) { @@ -29,6 +30,10 @@ func TestLanguageFromPURL(t *testing.T) { purl: "pkg:golang/github.com/gorilla/context@234fd47e07d1004f0aed9c", want: Go, }, + { + purl: "pkg:pub/util@1.2.34", + want: Dart, + }, { purl: "pkg:cargo/clap@2.33.0", want: Rust, diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index 10c86bdfe..efd11e3b0 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -17,6 +17,7 @@ const ( JavaMetadataType MetadataType = "JavaMetadata" NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata" RpmdbMetadataType MetadataType = "RpmdbMetadata" + DartPubMetadataType MetadataType = "DartPubMetadata" PythonPackageMetadataType MetadataType = "PythonPackageMetadata" RustCargoPackageMetadataType MetadataType = "RustCargoPackageMetadata" KbPackageMetadataType MetadataType = "KbPackageMetadata" @@ -31,6 +32,7 @@ var AllMetadataTypes = []MetadataType{ JavaMetadataType, NpmPackageJSONMetadataType, RpmdbMetadataType, + DartPubMetadataType, PythonPackageMetadataType, RustCargoPackageMetadataType, KbPackageMetadataType, @@ -45,6 +47,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{ JavaMetadataType: reflect.TypeOf(JavaMetadata{}), NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}), RpmdbMetadataType: reflect.TypeOf(RpmdbMetadata{}), + DartPubMetadataType: reflect.TypeOf(DartPubMetadata{}), PythonPackageMetadataType: reflect.TypeOf(PythonPackageMetadata{}), RustCargoPackageMetadataType: reflect.TypeOf(CargoMetadata{}), KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}), diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 396faf522..ffa965bb9 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -20,6 +20,7 @@ const ( GoModulePkg Type = "go-module" RustPkg Type = "rust-crate" KbPkg Type = "msrc-kb" + DartPubPkg Type = "dart-pub" ) // AllPkgs represents all supported package types @@ -36,6 +37,7 @@ var AllPkgs = []Type{ GoModulePkg, RustPkg, KbPkg, + DartPubPkg, } // PackageURLType returns the PURL package type for the current package. @@ -61,6 +63,8 @@ func (t Type) PackageURLType() string { return packageurl.TypeGolang case RustPkg: return "cargo" + case DartPubPkg: + return packageurl.TypePub default: // TODO: should this be a "generic" purl type instead? return "" @@ -98,6 +102,8 @@ func TypeByName(name string) Type { return GemPkg case "cargo", "crate": return RustPkg + case packageurl.TypePub: + return DartPubPkg default: return UnknownPkg } diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index d5f5bc3e4..ade1f0913 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -1,9 +1,10 @@ package pkg import ( - "github.com/scylladb/go-set/strset" "testing" + "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/assert" ) @@ -46,6 +47,10 @@ func TestTypeFromPURL(t *testing.T) { purl: "pkg:cargo/clap@2.33.0", expected: RustPkg, }, + { + purl: "pkg:pub/util@1.2.34?hosted_url=pub.hosted.org", + expected: DartPubPkg, + }, { purl: "pkg:composer/laravel/laravel@5.5.0", expected: PhpComposerPkg, diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index dad951b6b..7d1ca3381 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -25,6 +25,20 @@ func TestPackageURL(t *testing.T) { }, expected: "pkg:golang/github.com/anchore/syft@v0.1.0", }, + { + name: "pub", + pkg: Package{ + Name: "bad-name", + Version: "0.1.0", + Type: DartPubPkg, + Metadata: DartPubMetadata{ + Name: "name", + Version: "0.2.0", + HostedURL: "pub.hosted.org", + }, + }, + expected: "pkg:pub/name@0.2.0?hosted_url=pub.hosted.org", + }, { name: "python", pkg: Package{ diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index 82aa81a26..8d566afcb 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -186,6 +186,19 @@ var dirOnlyTestCases = []testCase{ "alcaeus/mongo-php-adapter": "1.1.11", }, }, + { + name: "find pubspec lock packages", + pkgType: pkg.DartPubPkg, + pkgLanguage: pkg.Dart, + pkgInfo: map[string]string{ + "ansicolor": "1.1.1", + "archive": "2.0.13", + "args": "1.6.0", + "key_binder": "1.11.20", + "ale": "3.3.0", + "analyzer": "0.40.7", + }, + }, } var commonTestCases = []testCase{ diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index b069a5cab..ade886512 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -1,9 +1,10 @@ package integration import ( - "github.com/stretchr/testify/require" "testing" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg/cataloger" "github.com/google/go-cmp/cmp" @@ -64,6 +65,7 @@ func TestPkgCoverageImage(t *testing.T) { // for image scans we should not expect to see any of the following package types definedLanguages.Remove(pkg.Go.String()) definedLanguages.Remove(pkg.Rust.String()) + definedLanguages.Remove(pkg.Dart.String()) observedPkgs := internal.NewStringSet() definedPkgs := internal.NewStringSet() @@ -75,6 +77,7 @@ func TestPkgCoverageImage(t *testing.T) { definedPkgs.Remove(string(pkg.KbPkg)) definedPkgs.Remove(string(pkg.GoModulePkg)) definedPkgs.Remove(string(pkg.RustPkg)) + definedPkgs.Remove(string(pkg.DartPubPkg)) var cases []testCase cases = append(cases, commonTestCases...) diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/dart/pubspec.lock b/test/integration/test-fixtures/image-pkg-coverage/pkgs/dart/pubspec.lock new file mode 100644 index 000000000..c7b912f1a --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/dart/pubspec.lock @@ -0,0 +1,49 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + ale: + dependency: transitive + description: + name: ale + url: "https://pub.hosted.org" + source: hosted + version: "3.3.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.40.7" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + key_binder: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "3f7b3a6350e73c7dcac45301c0e18fbd42af02f7" + url: "git@github.com:Workiva/key_binder.git" + source: git + version: "1.11.20" +sdks: + dart: ">=2.12.0 <3.0.0"