Safeguard
Developer Tools•

Submodule, vendored copy, or package: how should an SBOM record each?

Three ways code enters a repo, three ways an SBOM can miss it. A decision guide for recording submodules, vendored copies and normal dependencies.

Safeguard Security Team
6 min read

Record each by what identifies it. A normal dependency is identified by name and version from a registry. A vendored copy is identified by the module version in its manifest. A submodule is identified by a commit. Give each a component entry and note how it entered the build.

Three ways code arrives

Entry pathIdentityWhere the facts liveTypical SBOM gap
Package dependencyName and versionLockfileDev versus runtime scope
Vendored copyModule and versionVendor manifestLocal edits unrecorded
Git submoduleCommitParent repo pointer and .gitmodulesEmpty folder in checkout

The Git documentation says a submodule is stored in the parent as a particular commit, mode 160000, and that a plain clone leaves the submodule directory empty. The Go modules reference says go mod vendor copies needed packages into a vendor directory and writes vendor/modules.txt listing modules and versions. A lockfile, such as the one the Poetry documentation describes, records exact resolved versions.

How to decide what to write

  1. Find how the code arrived. Look at the lockfile, a vendor directory and .gitmodules.
  2. Pick the identity from the table above.
  3. Add a purl where an ecosystem exists. For a submodule with no package, use the repository URL and commit as external references.
  4. Record whether the copy is modified.
  5. Note where the original came from, so a reader can compare.

Edge cases

A library vendored by copying files with no manifest has no machine readable version. Pick a version from the upstream tag or commit you copied from, write it by hand, and flag it as manually entered.

A submodule that itself has a lockfile contains more components. Include those too, otherwise your SBOM hides the dependencies of code you ship.

A vendored directory that was edited is a fork in practice. CycloneDX has a pedigree concept for describing modified components, and your SBOM can note the change in plain text if your tool does not support it.

A checkout test

Run your SBOM tool on a fresh clone and on a clone with submodules initialized. Compare counts. If they differ, your CI checkout step needs submodule support. Then do the same with the vendor directory removed and restored, to see which source your tool reads.

Questions to ask a tool

  • Does it read vendor manifests, or just top level module files?
  • Does it follow submodules, or expect them present?
  • Can I add manual components that survive regeneration?
  • Does it record how each component was found?

A worked example

A team ships a command line tool. Its repository has all three entry paths in one place. A package manifest lists 20 registry dependencies. A vendor directory holds one Go style module copied in for offline builds. A submodule holds a parsing library maintained by a partner team. The counts are illustrative.

ItemEntry pathWhat you recordOpen question
20 registry packagesPackage dependencyName, version, purlDev or runtime scope
1 vendored moduleVendored copyModule, version, note on editsIs it modified
1 partner parserSubmoduleRepo URL, commit, own dependenciesDoes it have its own lockfile

An SBOM that lists solely the first row looks complete and hides two thirds of the interesting questions. Adding the other two takes under an hour the first time and a few minutes per release after that.

Writing the entry for each

For a registry package, your tool does the work. For a vendored copy, take the module path and version from the vendor manifest. For a submodule, create a component whose version is the commit and add an external reference to the repository. In each case add a short property saying how it entered the build: registry, vendored or submodule. A reader can then filter by entry path.

Handling modifications

Copies get edited. A patched vendored module is no longer the same as upstream, even with the same version string. State the base version, say that it is modified, and link to the patch or the commit that changed it. A reader checking advisories then knows that an upstream fix may not apply cleanly, and that a vulnerability may be present or absent compared with upstream.

Where licenses fit

Each entry path can carry a license. Registry packages usually declare one. A vendored copy keeps its license file inside the directory. A submodule has a license in its own repository. Collect all three, because a license review that covers solely the registry list is missing code that you ship.

Decision rules you can adopt

  1. If a package manager can fetch it, prefer the registry path over copying files.
  2. If you must vendor, vendor with a manifest and a note, never a bare file copy.
  3. If you use a submodule, pin it to a commit on a release tag when one exists.
  4. If you patch anything, record the patch in the SBOM.
  5. Review all three entry paths in every release review.

Quarterly cleanup

Every quarter, list the vendored and submodule entries and ask why each still exists. Many were added for a reason that no longer applies, such as an offline build that now has a mirror. Each one you convert to a normal dependency becomes easier to track and patch.

Common objections

Some teams say the extra entries add noise. The answer is that the entries are the cases where tools disagree most, which is why a human needs to see them. Others say a bare file copy is too small to track. Small copied files with licenses and security history are exactly the ones that surprise people during an audit.

Where Safeguard fits

Safeguard scans dependencies and SBOMs, and its free scan at gold.safeguard.sh/scan can show what a given checkout contains. Manual records for copied code are still your team's call.

Try it yourself

Click through it at your own pace, or open it full screen.

Click into a few components and note how direct and transitive packages are separated.

Quick check

What is the version of a submodule?

The commit the parent repository records. Add a tag as a note if the upstream has one.

Should an edited vendored library keep its upstream version?

Keep the upstream version but mark it as modified, and describe the change.

Is a copied single file a component?

If it carries code you did not write, treat it as one. Record its origin, version or commit, and license.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.