What happened:
A pnpm-lock.yaml that contains a devEngines.runtime entry (pnpm's Node runtime self-management) produces an empty SBOM. Syft exits 0 and writes a well-formed document, so nothing signals that every project dependency was dropped. On a real ~1200-package lockfile this silently yields 0 packages.
The cause is a type mismatch that is then treated as fatal for the whole document. pnpm records a provisioned runtime with a variations resolution, where variants is a sequence:
node@runtime:26.8.1:
resolution:
type: variations
variants:
- resolution:
archive: tarball
integrity: sha256-srdmYPpN7U4LKkHuPAxlHNUuqBcOrZHrrB4UesPVVkM=
type: binary
url: https://nodejs.org/download/release/v26.8.1/node-v26.8.1-linux-x64.tar.gz
targets:
- cpu: x64
os: linux
pnpmV9PackageEntry.Resolution is declared as map[string]string in syft/pkg/cataloger/javascript/parse_pnpm_lock.go, so the sequence cannot be decoded:
failed to unmarshal pnpm v9 lockfile: yaml: unmarshal errors:
line 29: cannot unmarshal !!seq into string
On its own that would be harmless — yaml.v3 reports this as a *yaml.TypeError and still populates every other entry. The problem is that Parse treats any error as fatal and discards the decoded result:
func (p *pnpmV9LockYaml) Parse(_ float64, doc *yaml.Node) ([]pnpmPackage, error) {
if err := doc.Decode(p); err != nil {
return nil, fmt.Errorf("failed to unmarshal pnpm v9 lockfile: %w", err)
}
So one unrepresentable entry throws away all the others. The multi-document loop then records the error and moves on, leaving nothing behind.
What you expected to happen:
The entries that decode correctly should still be cataloged. An entry Syft cannot represent should be skipped (ideally with a warning), not cause the rest of the lockfile to be dropped.
Steps to reproduce the issue:
pnpm-lock.yaml:
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
is-odd:
specifier: 3.0.1
version: 3.0.1
devDependencies:
node:
specifier: runtime:^26.8.1
version: runtime:26.8.1
packages:
is-odd@3.0.1:
resolution: {integrity: sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==}
engines: {node: '>=4'}
node@runtime:26.8.1:
resolution:
type: variations
variants:
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-srdmYPpN7U4LKkHuPAxlHNUuqBcOrZHrrB4UesPVVkM=
type: binary
url: https://nodejs.org/download/release/v26.8.1/node-v26.8.1-linux-x64.tar.gz
targets:
- cpu: x64
os: linux
version: 26.8.1
hasBin: true
snapshots:
is-odd@3.0.1: {}
node@runtime:26.8.1: {}
$ syft pnpm-lock.yaml -o json | jq '.artifacts | length'
0
$ echo $?
0
Deleting the node@runtime:26.8.1 entries yields the expected is-odd. The error is only visible at -vvv:
TRACE cataloger returned errors cataloger=javascript-lock-cataloger
error=/pnpm-lock.yaml: failed to parse pnpm-lock.yaml document 0:
failed to unmarshal pnpm v9 lockfile: yaml: unmarshal errors:
line 29: cannot unmarshal !!seq into string
Anything else we need to know?:
A possible fix is to tolerate *yaml.TypeError in pnpmV9LockYaml.Parse (and pnpmV6LockYaml.Parse) — log it and keep the entries that decoded — rather than returning early. Typing Resolution as map[string]any would additionally let variations resolutions decode cleanly.
This compounds with #5168. A lockfile can hit both at once: pnpm writes its package-manager block as a separate leading YAML document and a devEngines.runtime entry in the main one. With the #5168 fix in 1.51.1, such a lockfile now parses document 0 (pnpm's own binaries) and fails document 1 (the actual dependency tree), so the SBOM contains only pnpm's release binaries. We hit exactly this on a real project: 1176 packages became 19.
Because the result is a valid-looking, non-empty SBOM, this is easy to ship into dependency-graph or vulnerability tooling without noticing that coverage is gone.
Environment:
- Output of
syft version: 1.51.1 (also reproduces conceptually on 1.33.0, which fails differently — it parses the entry into pkg:npm/node@runtime%3A26.7.0)
- OS: Ubuntu (Linux 6.8.0, amd64)
What happened:
A
pnpm-lock.yamlthat contains adevEngines.runtimeentry (pnpm's Node runtime self-management) produces an empty SBOM. Syft exits 0 and writes a well-formed document, so nothing signals that every project dependency was dropped. On a real ~1200-package lockfile this silently yields 0 packages.The cause is a type mismatch that is then treated as fatal for the whole document. pnpm records a provisioned runtime with a variations resolution, where
variantsis a sequence:pnpmV9PackageEntry.Resolutionis declared asmap[string]stringinsyft/pkg/cataloger/javascript/parse_pnpm_lock.go, so the sequence cannot be decoded:On its own that would be harmless —
yaml.v3reports this as a*yaml.TypeErrorand still populates every other entry. The problem is thatParsetreats any error as fatal and discards the decoded result:So one unrepresentable entry throws away all the others. The multi-document loop then records the error and moves on, leaving nothing behind.
What you expected to happen:
The entries that decode correctly should still be cataloged. An entry Syft cannot represent should be skipped (ideally with a warning), not cause the rest of the lockfile to be dropped.
Steps to reproduce the issue:
pnpm-lock.yaml:Deleting the
node@runtime:26.8.1entries yields the expectedis-odd. The error is only visible at-vvv:Anything else we need to know?:
A possible fix is to tolerate
*yaml.TypeErrorinpnpmV9LockYaml.Parse(andpnpmV6LockYaml.Parse) — log it and keep the entries that decoded — rather than returning early. TypingResolutionasmap[string]anywould additionally let variations resolutions decode cleanly.This compounds with #5168. A lockfile can hit both at once: pnpm writes its package-manager block as a separate leading YAML document and a
devEngines.runtimeentry in the main one. With the #5168 fix in 1.51.1, such a lockfile now parses document 0 (pnpm's own binaries) and fails document 1 (the actual dependency tree), so the SBOM contains only pnpm's release binaries. We hit exactly this on a real project: 1176 packages became 19.Because the result is a valid-looking, non-empty SBOM, this is easy to ship into dependency-graph or vulnerability tooling without noticing that coverage is gone.
Environment:
syft version:1.51.1(also reproduces conceptually on 1.33.0, which fails differently — it parses the entry intopkg:npm/node@runtime%3A26.7.0)