Gunnar

mikronova/modgunn

.nornir/manifests-guide.md

# manifests — guide

> **Class:** `guide` · audience: someone turning a project's manifest/lockfile into components.
> The *what* is in [`manifests-design.md`](manifests-design.md); the emit side is
> [`sbom-guide.md`](sbom-guide.md). Needs `--features manifests`.

## f0 — parse a manifest by filename (the common path)

```rust
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`](sbom-guide.md) takes, so a
polyglot repo becomes one SBOM with correctly-tagged purls and no ecosystem-specific glue:

```rust
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:

```rust
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`:

```console
$ 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 literal `Version`, and a Gemfile constraint line
  are all skipped — they are not a pinned resolution.
- **`license` is populated only where the manifest carries it** (npm `package-lock.json`); every
  other ecosystem yields `license: None` (unknown). The version-authority is the lockfile, not a
  registry lookup — nothing here goes to the network.
- **`pnpm-lock.yaml` is hand-parsed** (no YAML dep): it reads the `packages:` block for both the v5
  `/name/version` and 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`](warehouse-guide.md)); to
  *emit* CycloneDX use [`sbom`](sbom-guide.md).