manifests — guide
Class:
guide· audience: someone turning a project's manifest/lockfile into components. The what is inmanifests-design.md; the emit side issbom-guide.md. Needs--features manifests.
f0 — parse a manifest by filename (the common path)
use modgunn::manifests::parse_manifest;
// Dispatches on the basename; a path is fine — only the last component matters.
let text = std::fs::read_to_string("Cargo.lock")?;
match parse_manifest("Cargo.lock", &text) {
Some(Ok(components)) => {
for c in &components {
println!("{}/{}@{}", c.ecosystem, c.name, c.version); // cargo/serde@1.0.203
}
}
Some(Err(e)) => eprintln!("malformed manifest: {e}"),
None => eprintln!("no ecosystem parser owns this filename"),
}
Works the same for every ecosystem — swap the filename and content: requirements.txt, poetry.lock, Pipfile.lock, package-lock.json, pnpm-lock.yaml, pom.xml, go.mod, go.sum, packages.lock.json, app.csproj, Gemfile.lock.
f1 — straight into a CycloneDX SBOM
The parsed Vec<SbomComponent> is exactly what sbom::to_cyclonedx takes, so a polyglot repo becomes one SBOM with correctly-tagged purls and no ecosystem-specific glue:
use modgunn::{manifests::parse_manifest, sbom::{to_cyclonedx, SbomInput}};
let mut components = Vec::new();
for (file, text) in manifests_on_disk { // (String, String)
if let Some(Ok(mut cs)) = parse_manifest(&file, &text) {
components.append(&mut cs);
}
}
let doc = to_cyclonedx(&SbomInput { subject: "my-repo".into(), components, vulns: vec![] });
// components[i].purl → pkg:cargo/…, pkg:pip/…, pkg:npm/…, pkg:go/…, …
f2 — call one ecosystem parser directly
When you already know the ecosystem, skip dispatch:
use modgunn::manifests::{CargoParser, EcosystemParser};
let comps = CargoParser.parse("Cargo.lock", &text)?; // Result<Vec<SbomComponent>, ParseError>
modgunn::manifests::all() returns every parser (Box<dyn EcosystemParser>) if you want to advertise or iterate the supported ecosystems.
f3 — from the command line (no Rust)
The same seam is a CLI verb, so you can turn a checked-out repo into components (or a full SBOM) without writing any code. Build the binary with the manifests feature and run ingest:
$ cargo run --features warehouse,manifests -- ingest --path Cargo.lock
cargo anyhow 1.0.86
cargo serde 1.0.203
2 component(s) from 1 manifest(s) [cargo 2]
# a whole repo (walks it, skipping .git/target/node_modules/…); one ecosystem only:
$ modgunn ingest --path ./my-repo --ecosystem pip
# emit a CycloneDX 1.5 SBOM instead of the listing:
$ modgunn ingest --path Cargo.lock --sbom --subject my-repo > sbom.json
Offline and zero-shell — it never runs the project's pip/npm/go/mvn. An explicit file no parser owns exits non-zero with the supported-manifest list; a malformed manifest is named on stderr and skipped, and the run still succeeds for the rest. Without the manifests feature the verb prints a one-line rebuild hint.
Gotchas
- Only resolved versions become components. A
requirements.txt>=/unpinned line, a Maven${property}version, a NuGet reference with no literalVersion, and a Gemfile constraint line are all skipped — they are not a pinned resolution. licenseis populated only where the manifest carries it (npmpackage-lock.json); every other ecosystem yieldslicense: None(unknown). The version-authority is the lockfile, not a registry lookup — nothing here goes to the network.pnpm-lock.yamlis hand-parsed (no YAML dep): it reads thepackages:block for both the v5/name/versionand v6+name@version(and scoped@scope/name@version) key spellings, dropping peer suffixes like(react@18.2.0).- This is the ingest side only. To read a repo's already-captured component set from the warehouse use
Warehouse::read_sbom_components(warehouse-guide.md); to emit CycloneDX usesbom.