feat: deno cataloger #4417 (#4523)

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
Rez Moss 2026-06-23 10:58:22 -04:00 committed by GitHub
parent 5eefd73ac7
commit fea4a50124
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 4881 additions and 4 deletions

View File

@ -3,7 +3,7 @@ package internal
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 = "16.1.4"
JSONSchemaVersion = "16.1.5"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@ -11,5 +11,6 @@ const (
// 16.1.2 - placeholder for 16.1.2 changelog
// 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
// 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
)

View File

@ -20,6 +20,8 @@ func AllTypes() []any {
pkg.CondaMetaPackage{},
pkg.DartPubspec{},
pkg.DartPubspecLockEntry{},
pkg.DenoLockEntry{},
pkg.DenoRemoteLockEntry{},
pkg.DotnetDepsEntry{},
pkg.DotnetPackagesLockEntry{},
pkg.DotnetPortableExecutableEntry{},

View File

@ -73,6 +73,8 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"),
jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"),
jsonNames(pkg.DartPubspec{}, "dart-pubspec"),
jsonNames(pkg.DenoLockEntry{}, "deno-lock-entry"),
jsonNames(pkg.DenoRemoteLockEntry{}, "deno-remote-lock-entry"),
jsonNames(pkg.DotnetDepsEntry{}, "dotnet-deps-entry", "DotnetDepsMetadata"),
jsonNames(pkg.DotnetPortableExecutableEntry{}, "dotnet-portable-executable-entry"),
jsonNames(pkg.DpkgArchiveEntry{}, "dpkg-archive-entry"),

View File

@ -105,7 +105,7 @@ func DefaultPackageTaskFactories() Factories {
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return javascript.NewLockCataloger(cfg.PackagesConfig.JavaScript)
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, JavaScript, Node, NPM,
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, JavaScript, Node, NPM, "deno",
),
newSimplePackageTaskFactory(php.NewComposerLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "php", "composer"),
newSimplePackageTaskFactory(php.NewPearCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, pkgcataloging.ImageTag, "php", "pear"),

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.1.4/document",
"$id": "anchore.io/schema/syft/json/16.1.5/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -748,6 +748,45 @@
],
"description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file."
},
"DenoLockEntry": {
"properties": {
"integrity": {
"type": "string",
"description": "Integrity is the crpto hash of the package content for verification"
},
"dependencies": {
"items": {
"type": "string"
},
"type": "array",
"description": "Dependencies is the list of package specifiers that this package depends on"
}
},
"type": "object",
"required": [
"integrity",
"dependencies"
],
"description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file"
},
"DenoRemoteLockEntry": {
"properties": {
"url": {
"type": "string",
"description": "URL is the remote URL from which the module fetcef"
},
"integrity": {
"type": "string",
"description": "Integrity is the crpto hash of the package content for verification"
}
},
"type": "object",
"required": [
"url",
"integrity"
],
"description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file"
},
"Descriptor": {
"properties": {
"name": {
@ -2658,6 +2697,12 @@
{
"$ref": "#/$defs/DartPubspecLockEntry"
},
{
"$ref": "#/$defs/DenoLockEntry"
},
{
"$ref": "#/$defs/DenoRemoteLockEntry"
},
{
"$ref": "#/$defs/DotnetDepsEntry"
},

View File

@ -57,6 +57,8 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.YarnLockEntry{},
pkg.TerraformLockProviderEntry{},
pkg.GGUFFileHeader{},
pkg.DenoLockEntry{},
pkg.DenoRemoteLockEntry{},
)
tests := []struct {
name string

View File

@ -22,6 +22,7 @@ catalogers:
config: javascript.CatalogerConfig # AUTO-GENERATED
selectors: # AUTO-GENERATED
- declared
- deno
- directory
- javascript
- language
@ -29,6 +30,44 @@ catalogers:
- npm
- package
parsers: # AUTO-GENERATED structure
- function: parseDenoLock
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
- '**/deno.lock'
metadata_types: # AUTO-GENERATED
- pkg.DenoLockEntry
- pkg.DenoRemoteLockEntry
- pkg.NpmPackageLockEntry
package_types: # AUTO-GENERATED
- npm
purl_types: # AUTO-GENERATED
- npm
json_schema_types: # AUTO-GENERATED
- DenoLockEntry
- DenoRemoteLockEntry
- JavascriptNpmPackageLockEntry
capabilities: # MANUAL - preserved across regeneration
- name: license
default: false
- name: dependency.depth
default:
- direct
- indirect
- name: dependency.edges
default: ""
- name: dependency.kinds
default:
- runtime
- name: package_manager.files.listing
default: false
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: true
evidence:
- DenoLockEntry.Integrity
- DenoRemoteLockEntry.Integrity
- function: parseBunLock
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED

View File

@ -20,9 +20,11 @@ func NewLockCataloger(cfg CatalogerConfig) pkg.Cataloger {
packageLockAdapter := newGenericPackageLockAdapter(cfg)
pnpmLockAdapter := newGenericPnpmLockAdapter(cfg)
bunLockAdapter := newGenericBunLockAdapter(cfg)
denoLockAdapter := newGenericDenoLockAdapter(cfg)
return generic.NewCataloger("javascript-lock-cataloger").
WithParserByGlobs(packageLockAdapter.parsePackageLock, "**/package-lock.json").
WithParserByGlobs(yarnLockAdapter.parseYarnLock, "**/yarn.lock").
WithParserByGlobs(pnpmLockAdapter.parsePnpmLock, "**/pnpm-lock.yaml").
WithParserByGlobs(bunLockAdapter.parseBunLock, "**/bun.lock")
WithParserByGlobs(bunLockAdapter.parseBunLock, "**/bun.lock").
WithParserByGlobs(denoLockAdapter.parseDenoLock, "**/deno.lock")
}

View File

@ -173,6 +173,7 @@ func Test_LockCataloger_Globs(t *testing.T) {
name: "obtain package files",
fixture: "testdata/glob-paths",
expected: []string{
"src/deno.lock",
"src/package-lock.json",
"src/pnpm-lock.yaml",
"src/yarn.lock",

View File

@ -0,0 +1,296 @@
package javascript
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)
type denoLock struct {
Version string `json:"version"`
Jsr map[string]denoJsrPackage `json:"jsr"`
Npm map[string]denoNpmPackage `json:"npm"`
Remote map[string]string `json:"remote"`
}
type denoJsrPackage struct {
Integrity string `json:"integrity"`
Dependencies []string `json:"dependencies"`
}
type denoNpmPackage struct {
Integrity string `json:"integrity"`
Dependencies []string `json:"dependencies"`
}
type genericDenoLockAdapter struct {
cfg CatalogerConfig
}
func newGenericDenoLockAdapter(cfg CatalogerConfig) genericDenoLockAdapter {
return genericDenoLockAdapter{
cfg: cfg,
}
}
func (a genericDenoLockAdapter) parseDenoLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
dec := json.NewDecoder(reader)
var lock denoLock
for {
if err := dec.Decode(&lock); errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, nil, fmt.Errorf("failed to parse deno.lock file: %w", err)
}
}
for nameVersion, pkgMeta := range lock.Jsr {
name, version := parseDenoJsrNameVersion(nameVersion)
if name == "" || version == "" {
continue
}
pkgs = append(pkgs, newDenoJsrPackage(reader.Location, name, version, pkgMeta))
}
for nameVersion, pkgMeta := range lock.Npm {
name, version := parseDenoNpmNameVersion(nameVersion)
if name == "" || version == "" {
continue
}
pkgs = append(pkgs, newDenoNpmPackage(reader.Location, name, version, pkgMeta))
}
for rawURL, integrity := range lock.Remote {
name, version := parseDenoRemoteURL(rawURL)
if name == "" {
continue
}
pkgs = append(pkgs, newDenoRemotePackage(reader.Location, name, version, rawURL, integrity))
}
pkg.Sort(pkgs)
return pkgs, dependency.Resolve(denoLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages")
}
func parseDenoJsrNameVersion(nameVersion string) (name, version string) {
idx := strings.LastIndex(nameVersion, "@")
if idx <= 0 {
return "", ""
}
return nameVersion[:idx], nameVersion[idx+1:]
}
func parseDenoNpmNameVersion(nameVersion string) (name, version string) {
if strings.HasPrefix(nameVersion, "@") {
rest := nameVersion[1:]
idx := strings.LastIndex(rest, "@")
if idx <= 0 {
return "", ""
}
return nameVersion[:idx+1], rest[idx+1:]
}
idx := strings.LastIndex(nameVersion, "@")
if idx <= 0 {
return "", ""
}
return nameVersion[:idx], nameVersion[idx+1:]
}
func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: denoJsrPackageURL(name, version),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.DenoLockEntry{
Integrity: meta.Integrity,
Dependencies: meta.Dependencies,
},
}
p.SetID()
return p
}
func newDenoNpmPackage(location file.Location, name, version string, meta denoNpmPackage) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: denoNpmPackageURL(name, version),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{
Integrity: meta.Integrity,
},
}
p.SetID()
return p
}
func newDenoRemotePackage(location file.Location, name, version, rawURL, integrity string) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: denoRemotePackageURL(name, version, rawURL),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.DenoRemoteLockEntry{
URL: rawURL,
Integrity: integrity,
},
}
p.SetID()
return p
}
func parseDenoRemoteURL(rawURL string) (name, version string) {
rawURL = strings.TrimPrefix(rawURL, "https://")
rawURL = strings.TrimPrefix(rawURL, "http://")
atIdx := strings.Index(rawURL, "@")
if atIdx == -1 {
slashIdx := strings.Index(rawURL, "/")
if slashIdx == -1 {
return rawURL, ""
}
return rawURL[:slashIdx], ""
}
name = rawURL[:atIdx]
rest := rawURL[atIdx+1:]
slashIdx := strings.Index(rest, "/")
if slashIdx == -1 {
version = rest
} else {
version = rest[:slashIdx]
}
return name, version
}
func extractRepositoryBase(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return ""
}
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
func denoRemotePackageURL(name, version, rawURL string) string {
repositoryURL := extractRepositoryBase(rawURL)
var qualifiers packageurl.Qualifiers
if repositoryURL != "" {
qualifiers = packageurl.Qualifiers{{Key: "repository_url", Value: repositoryURL}}
}
return packageurl.NewPackageURL(
packageurl.TypeNPM,
"",
name,
version,
qualifiers,
"",
).ToString()
}
func denoJsrPackageURL(name, version string) string {
var namespace string
fields := strings.SplitN(name, "/", 2)
if len(fields) > 1 {
namespace = fields[0]
name = fields[1]
}
return packageurl.NewPackageURL(
packageurl.TypeNPM,
namespace,
name,
version,
packageurl.Qualifiers{{Key: "repository_url", Value: "https://jsr.io"}},
"",
).ToString()
}
func denoNpmPackageURL(name, version string) string {
var namespace string
fields := strings.SplitN(name, "/", 2)
if len(fields) > 1 {
namespace = fields[0]
name = fields[1]
}
return packageurl.NewPackageURL(
packageurl.TypeNPM,
namespace,
name,
version,
nil,
"",
).ToString()
}
func denoLockDependencySpecifier(p pkg.Package) dependency.Specification {
meta, ok := p.Metadata.(pkg.DenoLockEntry)
if !ok {
return dependency.Specification{}
}
provides := []string{p.Name}
var requires []string
for _, dep := range meta.Dependencies {
name := parseDenoDependencyName(dep)
if name != "" {
requires = append(requires, name)
}
}
return dependency.Specification{
ProvidesRequires: dependency.ProvidesRequires{
Provides: provides,
Requires: requires,
},
}
}
func parseDenoDependencyName(dep string) string {
if strings.HasPrefix(dep, "jsr:") {
dep = strings.TrimPrefix(dep, "jsr:")
} else if strings.HasPrefix(dep, "npm:") {
dep = strings.TrimPrefix(dep, "npm:")
}
if strings.HasPrefix(dep, "@") {
rest := dep[1:]
atIdx := strings.Index(rest, "@")
if atIdx > 0 {
return dep[:atIdx+1]
}
return dep
}
idx := strings.Index(dep, "@")
if idx > 0 {
return dep[:idx]
}
return dep
}

View File

@ -0,0 +1,83 @@
package javascript
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParseDenoLock(t *testing.T) {
fixture := "test-fixtures/deno/deno.lock"
expectedPkgs := []pkg.Package{
{
Name: "@std/bytes",
Version: "1.0.2",
PURL: "pkg:npm/%40std/bytes@1.0.2?repository_url=https%3A%2F%2Fjsr.io",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.DenoLockEntry{
Integrity: "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57",
},
},
{
Name: "@std/encoding",
Version: "1.0.5",
PURL: "pkg:npm/%40std/encoding@1.0.5?repository_url=https%3A%2F%2Fjsr.io",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.DenoLockEntry{
Integrity: "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04",
Dependencies: []string{"jsr:@std/bytes@^1.0.0"},
},
},
{
Name: "chalk",
Version: "5.3.0",
PURL: "pkg:npm/chalk@5.3.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{
Integrity: "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
},
},
{
Name: "deno.land/std",
Version: "0.140.0",
PURL: "pkg:npm/deno.land%2Fstd@0.140.0?repository_url=https%3A%2F%2Fdeno.land",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.DenoRemoteLockEntry{
URL: "https://deno.land/std@0.140.0/path/mod.ts",
Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
},
},
}
for i := range expectedPkgs {
expectedPkgs[i].Locations.Add(file.NewLocation(fixture))
}
// @std/encoding depends => @std/bytes
expectedRelationships := []artifact.Relationship{
{
From: expectedPkgs[0], // @std/bytes main
To: expectedPkgs[1], // @std/encoding dep
Type: artifact.DependencyOfRelationship,
},
}
adapter := newGenericDenoLockAdapter(DefaultCatalogerConfig())
pkgtest.TestFileParser(t, fixture, adapter.parseDenoLock, expectedPkgs, expectedRelationships)
}
func Test_corruptDenoLock(t *testing.T) {
adapter := newGenericDenoLockAdapter(DefaultCatalogerConfig())
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/deno/corrupt/deno.lock").
WithError().
TestParser(t, adapter.parseDenoLock)
}

View File

@ -0,0 +1,4 @@
{
"version": "4",
this is not valid json
}

View File

@ -0,0 +1,27 @@
{
"version": "4",
"specifiers": {
"jsr:@std/bytes@^1.0.0": "1.0.2",
"jsr:@std/encoding@^1.0.0": "1.0.5",
"npm:chalk@5": "5.3.0"
},
"jsr": {
"@std/bytes@1.0.2": {
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
},
"@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04",
"dependencies": [
"jsr:@std/bytes@^1.0.0"
]
}
},
"npm": {
"chalk@5.3.0": {
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
}
},
"remote": {
"https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d"
}
}

View File

@ -0,0 +1,3 @@
{
"version": "4"
}

19
syft/pkg/deno.go Normal file
View File

@ -0,0 +1,19 @@
package pkg
// DenoLockEntry is a struct that rep a single entry found in the "packages" section of a Deno deno.lock file
type DenoLockEntry struct {
// Integrity is the crpto hash of the package content for verification
Integrity string `mapstructure:"integrity" json:"integrity"`
// Dependencies is the list of package specifiers that this package depends on
Dependencies []string `mapstructure:"dependencies" json:"dependencies"`
}
// DenoRemoteLockEntry is a struct that rep a single entry found in the "remote" section of a Deno deno.lock file
type DenoRemoteLockEntry struct {
// URL is the remote URL from which the module fetcef
URL string `mapstructure:"url" json:"url"`
// Integrity is the crpto hash of the package content for verification
Integrity string `mapstructure:"integrity" json:"integrity"`
}