Adding a New Ecosystem
Dependency discovery is 100% native Rust — there is no external scanner. Two pieces ship per ecosystem:
- A cataloger that reads the ecosystem’s lockfile / install tree / manifest
and emits
Dependencyrecords (dependency discovery). - A registry parser that reads the ecosystem’s registry config files and
emits
RegistryConfigrecords (registry-provenance validation).
Both are in-process Rust. The cataloger implements the Cataloger trait; the
registry parser plugs into attach_ecosystem_registries().
Architecture overview
Section titled “Architecture overview”flowchart TD A[project_roots.rs finds project directory] --> B["catalogers.dispatch(root)\nnative catalogers → merged PackageMetadata"] B --> D["attach_ecosystem_registries()\nRead .npmrc / NuGet.Config / pip.conf / etc."] D --> E[vuln.rs enriches vulnerabilities\nvia OSV by PURL] E --> F[policy.rs evaluates rules\nseverity, licenses, registries] F --> G[Return PackageMetadata]The cataloger you write plugs into step B; the registry parser plugs into step D. Everything downstream is handled by existing infrastructure.
Choosing a cataloger tier
Section titled “Choosing a cataloger tier”A cataloger declares its tier via fn tier() -> CatalogerTier. Dispatch
suppresses lower tiers when a higher one matches the same project root, so you
can ship a Naked tier first and add a Lockfile tier later without coordination.
| Tier | When | Emit |
|---|---|---|
| Lockfile | A pinned lockfile exists (package-lock.json, Cargo.lock, composer.lock, Gemfile.lock, …). Most ecosystems land here. | Resolution::Locked |
| InstallTreeOnly | No lockfile but a populated install cache (~/.m2/repository/, ~/.gradle/caches/). Reserved tier — currently only Maven. | Resolution::Locked / Resolution::Walked |
| Naked | Just the manifest with version constraints (package.json without a lockfile, pom.xml, naked build.gradle). | Resolution::Constraint |
The ~20 shipped catalogers under app/core/src/discovery/catalogers/ are the
reference patterns — pick the one with the closest format shape and copy its
structure.
Checklist
Section titled “Checklist”Replace <lang>/<pm> with your ecosystem / package manager (e.g., python
/ pip, rust / cargo).
Cataloger
- Create
app/core/src/discovery/catalogers/<ecosystem>/<pm>.rsimplementing theCatalogertrait (name(),tier(),matches(root),parse(root, opts)). - Register it in
default_registry()inapp/core/src/discovery/catalogers.rs. - Add the ecosystem’s marker files to
discovery::project_roots::PROJECT_MARKERSif not already listed.
Registry parser
- Read the Registry Conventions page for the full spec of what config files exist at each level for your ecosystem.
- Create
app/core/src/languages/<lang>/registry.rs. - If
app/core/src/languages/<lang>.rsdoesn’t exist yet, create it with justpub mod registry;. - Wire
pub mod <lang>;inapp/core/src/languages.rs. - Implement the parser (see below).
- Wire it into
attach_ecosystem_registries()inapp/core/src/lib.rs.
Tests
- Add unit tests in
#[cfg(test)]modules in both the cataloger andregistry.rs. - Add a fixture directory under
app/core/tests/fixtures/<lang>/. - Add an integration test in
app/core/tests/integration.rsthat asserts both the cataloger output AND the registry attachment.
Registry parser implementation
Section titled “Registry parser implementation”File: app/core/src/languages/<lang>/registry.rs
Section titled “File: app/core/src/languages/<lang>/registry.rs”use crate::languages::language_plugin::RegistryConfig;use crate::utils::hash_secret;use std::fs;use std::path::Path;
/// Parse <lang> registry configuration from a project directory./// Returns one `RegistryConfig` per discovered registry URL.pub fn parse_<lang>_registries(project_dir: &Path) -> Vec<RegistryConfig> { let mut registries = Vec::new();
// 1. Project-level config let config_path = project_dir.join("<config-file>"); if config_path.exists() { // Parse it, push RegistryConfig entries... }
// 2. User-level config if let Some(home) = dirs::home_dir() { let user_config = home.join("<user-level-path>"); if user_config.exists() { // Parse it, push RegistryConfig entries... } }
registries}Contract
Section titled “Contract”Every registry parser must:
- Return
Vec<RegistryConfig>— one entry per registry URL found. - Check both project and user levels (and machine level where it exists).
- Hash credentials via
utils::hash_secret()before storing inpassword_hash. Never store plaintext. - Short-circuit with an empty
Vecwhen the config file is absent. - Never panic — use
Option/Resultand return empty on failure. - Set
valid: trueon every emitted record. Thevalidfield is overwritten later by policy evaluation.
Wiring into the dispatch
Section titled “Wiring into the dispatch”In app/core/src/lib.rs, find attach_ecosystem_registries and add
your parser alongside the existing ones:
// ---- <Lang> (<config-file>) ----------------------------registries.extend(languages::<lang>::registry::parse_<lang>_registries( project_root,));The function is called for EVERY project root regardless of ecosystem, so your parser must short-circuit quickly when its config file is missing.
.NET .csproj fallback (special case)
Section titled “.NET .csproj fallback (special case)”.NET is the one cataloger that wraps a legacy parser: the NuGet cataloger
handles packages.lock.json first, then falls through to an in-process
.csproj + packages.config parser when the lockfile is absent. New
ecosystems should not model on this — write a primary cataloger at the
Lockfile tier and (optionally) a Naked-tier fallback that emits
Resolution::Constraint.
Shared file formats
Section titled “Shared file formats”Several ecosystems share the same config file format. Reuse existing parsers where possible:
| Format | Crate | Used by |
|---|---|---|
| INI | line-by-line (custom) | Node, Python, Java |
| TOML | toml | Python, Rust |
| YAML | serde_yaml | Node (Berry), Dart, Ruby |
| XML | quick-xml | .NET, Java (Maven) |
| netrc | shared utility | Go, Swift, Python/uv |
| JSON | serde_json | PHP, Dart |
Example: Node.js registry parser
Section titled “Example: Node.js registry parser”The Node parser at app/core/src/languages/node/registry.rs is the
most complete reference implementation. It handles:
- Top-level
registry=<url>(default) @scope:registry=<url>(scoped)//<host><path>/:_authToken=<token>(modern bearer auth)<key>_auth=<base64>(legacy HTTP-basic).yarnrc.ymlnpmRegistryServer+npmScopes.<scope>.npmRegistryServer
Plus project, user, and XDG-level config sources, credential hashing, and comment handling.
Start there when implementing a new ecosystem’s parser.