--- name: packages description: Look up every package this repository publishes across all registries via packages.ecosyste.ms, with downloads, dependent counts, latest version, registry URL and supply-chain risk flags. license: MIT compatibility: Prefers the scrutineer API's cached ecosyste.ms payload; needs network access to packages.ecosyste.ms only when nothing is cached. metadata: scrutineer.version: 1 scrutineer.output_file: report.json scrutineer.output_kind: packages scrutineer.requires_remote: true --- # packages One repository can ship multiple packages across multiple ecosystems. Ask packages.ecosyste.ms for all of them and record the headline stats. ## Workspace - `./context.json` — has `repository.url`, plus `scrutineer.api_base`, `scrutineer.token` and `scrutineer.repository_id` - `./report.json` — write the packages array here - `./schema.json` — output shape ## What to do 1. Read `./context.json` and extract `repository.url`, `scrutineer.api_base`, `scrutineer.token` and `scrutineer.repository_id`. 2. Ask scrutineer for the cached payload first: `GET {api_base}/repositories/{repository_id}/ecosystems/packages/raw` with the bearer token. A 200 is the verbatim upstream lookup response — use it and go to step 4. This is the only path that works under `--hardened`, where ecosyste.ms is not in the egress allowlist, and it saves re-fetching what the prefetcher already collated. 3. On 404 nothing is cached — the steady state under `ecosystems_enrichment: false` — so fetch `https://packages.ecosyste.ms/api/v1/packages/lookup?repository_url={URL-ENCODED_URL}` directly. Either way the payload is a JSON array, one object per published package. 4. For each package upstream returns, emit one entry in `report.json` under `packages` mapping these fields: - `name` from upstream `name` - `ecosystem` from upstream `ecosystem` (e.g. `rubygems`, `npm`, `pypi`) - `purl` from upstream `purl` - `licenses` from upstream `licenses` (string, comma-joined if upstream gives a list) - `latest_version` from upstream `latest_release_number` or `latest_version` - `versions_count` from upstream `versions_count` - `downloads` from upstream `downloads` - `dependent_packages` from upstream `dependent_packages_count` - `dependent_repos` from upstream `dependent_repos_count` - `registry_url` from upstream `registry_url` or the registry's canonical package page - `latest_release_at` from upstream `latest_release_published_at` (RFC3339) - `dependent_packages_url` from upstream `dependent_packages_url` - `metadata` — the whole upstream object for this package, verbatim - `risk_flags`, see "Risk flags" below If upstream answered and this repository genuinely publishes no packages (the lookup returns `[]`), write `{"packages": []}`. That is a valid result, not an error. Only write that empty array when a source actually answered. If neither source is reachable — the cached endpoint returned 404 *and* the direct fetch failed, which is what `--hardened` looks like on an uncached repository — do not write `./report.json` at all. Exit non-zero instead. The parser replaces the whole package set for the repository, deleting every existing row before inserting what you produced, so an empty array reported as success wipes packages a previous scan recorded rather than leaving them untouched. ## Risk flags Each entry in `risk_flags` is `{"id": ..., "evidence": ...}`. `id` is one of the five ids listed below. `evidence` is one sentence naming what you actually saw (the file, the field, the date), not a restatement of the flag name. The five flags and how to check each: - `single_maintainer`: the registry lists one owner or publisher for this package. Read it from the upstream payload's maintainers/owners field. - `no_security_policy`: no `SECURITY.md` (or `.github/SECURITY.md`, `docs/SECURITY.md`) in `./src`. - `native_extension`: the package ships compiled code, so installing it runs a toolchain. Examples worth naming: a `binding.gyp` or `node-gyp` dependency, `ext/` with an `extconf.rb`, `setup.py` declaring `ext_modules`, a `build.rs`, cgo directives. - `stale_release`: the latest release is more than eighteen months older than today. Compute it from the upstream `latest_release_published_at`. - `maintainer_domain_expired`: a maintainer contact's email domain no longer resolves. Omit a flag you could not check. An absent flag means "not checked or not found", never "checked and clean". Guessing is worse than silence here, because an analyst reads every flag as something you actually verified. `./src` is the repository checkout, available to this skill, so the source-derived checks read from there. Under `--hardened` the DNS check for `maintainer_domain_expired` is unavailable, so that flag is simply omitted rather than guessed. Write `risk_flags` as an empty array or omit it entirely when nothing applies; both are valid.