Skip to content

Classify scope from the declared root and from syft lock entries - #25

Merged
andrew merged 5 commits into
git-pkgs:mainfrom
simonchrz:fix/declared-scope-root
Nov 2, 2026
Merged

andrew merged 5 commits into
git-pkgs:mainfrom
simonchrz:fix/declared-scope-root

Conversation

@simonchrz

@simonchrz simonchrz commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23.

  • Document.Component.ID keeps the CycloneDX metadata.component bom-ref (and is written back by Encode).
  • When a declared root, that ID or a DESCRIBES target, has DEPENDS_ON edges, it is the only root. Other nodes without dependents are no longer taken as roots.
  • Without one, roots are inferred as before. As you noted, the graph alone can't tell a lock file's project node from a top-level dependency, so this uses the lock file syft records per package: the syft:location:0:path property in CycloneDX, the path ending PackageSourceInfo in SPDX. I ran syft 1.55.0 over a small project per lock format:
lock file lists the project before after
package-lock.json, Cargo.lock, uv.lock, yarn berry yes correct unchanged
poetry.lock, pdm.lock, pnpm-lock.yaml, yarn v1 no top-level dep taken as root top-level dep direct

yarn v1 and berry both write yarn.lock, so one that has a berry workspace entry (version 0.0.0-use.local) is left to inference.

  • syft writes SPDX dependency edges as DEPENDENCY_OF, which the parser kept as they were, so ClassifyScope returned an empty map for every syft SPDX document. Both SPDX parsers now normalise B DEPENDENCY_OF A to A DEPENDS_ON B, as the README already says they normalise dependency edges. PackageSourceInfo is kept as Package.SourceInfo.
  • Roots() returns the root IDs and whether they were declared, so a caller can tell the project from an inferred root. The README describes all of this.

The syft outputs, CycloneDX and SPDX, are in testdata/*/syft-lock. The projectless cases fail without the change in both formats, and every SPDX case did before.

ClassifyScope took any node with dependencies and no dependents as a
root. For a lock file that doesn't list the project, such as poetry.lock,
that made the top-level dependencies roots, so they dropped out and their
dependencies were reported as direct.

The CycloneDX metadata.component bom-ref is now kept as
Document.Component.ID. A declared root (that or a DESCRIBES target) with
dependency edges is the only root. Without one, roots are inferred as
before, except that packages syft records as coming from poetry.lock,
pdm.lock, pnpm-lock.yaml or a yarn v1 yarn.lock are direct dependencies
rather than roots. Roots reports the root IDs and whether they were
declared.

Fixes git-pkgs#23
syft writes SPDX dependency edges as DEPENDENCY_OF, which the parser
kept as they were, so ClassifyScope found no graph. The SPDX parsers now
normalise "B DEPENDENCY_OF A" to "A DEPENDS_ON B", as the README
already said they normalise dependency edges.

PackageSourceInfo is kept as Package.SourceInfo, and the lock file
detection reads the path at its end when there is no syft location
property, so it works for syft's SPDX output too. It now goes by the
lock file's name for both formats rather than syft's metadata type.

@andrew andrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct the Roots() declaration flag and add regression coverage for explicitly declared roots without outgoing dependency edges.

Comment thread sbom.go Outdated
roots[id] = true
}
}
return roots, top, false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns declared=false even when every returned root is explicitly declared. For example, parsing an SPDX document with DOCUMENT DESCRIBES app and no dependency edges makes Roots() return ([app], false). The supplied Poetry fixtures also return only the declared directory root with false after the lock-file heuristic excludes the inferred dependency roots. Callers therefore cannot use the flag to identify the declared project as documented. Please derive the flag from the returned roots and add regression tests for these cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9dced53. declared is now true when every returned root is declared, so a DESCRIBES target without edges gives ([app], true), and so do the poetry, pdm, pnpm and yarn v1 fixtures. A declared root next to an inferred one still gives false. Added tests for the SPDX case and the mixed case, and the syft fixtures now check the flag.

Roots returned declared=false whenever the declared root had no edges
of its own, even when no other root was inferred: an SPDX document
that only DESCRIBES the app, or syft's output for a poetry, pdm, pnpm
or yarn v1 lock file, where the scanned directory is the only root.
The flag now says whether every returned root is declared.
@simonchrz

Copy link
Copy Markdown
Contributor Author

Thanks, fixed the declared flag in 9dced53 and added the regression tests. Ready for another look.

A top-level dependency without dependencies of its own appears in no
DEPENDS_ON edge, so it was never inferred as a root and got no scope. In
a lock file that leaves the project out, such a package is a direct
dependency. It is classified so only when other packages of its lock file
have edges, so a lock file the graph leaves out stays unclassified.
@simonchrz

Copy link
Copy Markdown
Contributor Author

One more case in da468b1, found with syft 1.55.0: a top-level dependency with no dependencies of its own (six in a poetry.lock) is in no DEPENDS_ON edge, so it was never inferred as a root and got no scope. Packages from a lock file that leaves the project out and that no edge names are now direct, but only when other packages of that lock file have edges, so a lock file the graph leaves out stays unclassified. New fixtures syft-lock/poetry-leaf in both formats, plus TestClassifyScopeLockFileWithoutEdges for the second part.

I also checked a monorepo with two poetry.lock files and a package-lock.json: syft writes one package per location, and the scopes come out right without changes.

@andrew
andrew merged commit 61e5aa3 into git-pkgs:main Nov 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClassifyScope treats top-level dependencies as roots when the lock file doesn't list the project

2 participants