Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ GitHits also exposes indexed package/source tools:
| `docs_list` | Browse mixed package documentation pages |
| `docs_read` | Read a documentation page by page ID |
| `pkg_info` | Quick package triage: version, license, repository popularity, downloads, vulnerabilities |
| `pkg_vulns` | CVE / OSV advisories for a package or specific version |
| `pkg_vulns` | CVE / OSV advisories for a package or specific version, with filter echo and compact/verbose modes |
| `pkg_deps` | Direct dependencies, dependency groups, and optional transitive graph |
| `pkg_changelog` | Release notes / changelog entries for a package or GitHub repo |
| `code_files` | Discover what files a dependency or repo contains |
Expand Down
12 changes: 6 additions & 6 deletions docs/implementation/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,27 +144,27 @@ githits pkg vulns crates:serde --json
githits pkg vulns npm:minimatch --include-withdrawn --verbose
```

Lists known CVE / OSV advisories for a package: severity, affected version ranges, fix versions, and upgrade targets. Malicious-package advisories (supply-chain attacks flagged by OSV) surface in a separate `MALWARE` bucket that sorts above all CVE advisories.
Lists known CVE / OSV advisories for a package: severity, affected version ranges, fix versions, and upgrade targets. Default text is capped at 5 advisory rows for readability; use `--verbose` for all rows or `--json` for the complete structured envelope. Malicious-package advisories (supply-chain attacks flagged by OSV) surface in a separate `MALWARE` bucket that sorts above all CVE advisories.

**Package spec.** `<registry>:<name>[@<version>]`. Unlike `pkg info`, `pkg vulns` supports `@<version>` so callers can inspect older pinned releases. Only `npm`, `pypi`, `hex`, and `crates` support vulnerability data; other registries are rejected client-side with `pkg vulns only supports npm, pypi, hex, and crates. Got: ${registry}.`

**Filtering.** `--severity low|medium|high|critical` maps to a CVSS float threshold (`low=0.1, medium=4, high=7, critical=9`) and is applied by the service. The returned `vulnerabilityCount` reflects the filtered set — no client-side filtering, no dual-summary block. Callers wanting the full picture omit the flag. `--include-withdrawn` includes retracted advisories; withdrawn advisories bucket below active ones in the terminal list.
**Filtering.** `--severity low|medium|high|critical` maps to a CVSS float threshold (`low=0.1, medium=4, high=7, critical=9`) and is applied by the service. The returned `vulnerabilityCount` reflects the filtered set — no client-side filtering, no dual-summary block. Callers wanting the full picture omit the flag. Active filters are echoed in text and under top-level JSON `filter`. `--include-withdrawn` includes retracted advisories; withdrawn advisories bucket below active ones in the terminal list.

**Zero-vulns hot path.** The common case (clean package) renders as header + one-line summary body (`No known vulnerabilities.`) — no breakdown, no advisory list, no footer. Agents checking "am I safe?" pay minimal token cost on the happy path.
**Zero-vulns hot path.** The common case (clean package) renders as header + one-line summary body (`No active vulnerabilities affect this version.`) — no breakdown, no advisory list, no footer. Filtered zero-results say `No vulnerabilities matching the filter affect this version.` so callers do not confuse a thresholded query with a clean package.

**Version validation.** `pkg vulns` expects canonical package versions. Tag-style inputs such as `@v4.18.0` are rejected client-side with `INVALID_ARGUMENT` and an actionable message telling the caller to drop the leading `v`, instead of forwarding the request and surfacing an opaque upstream failure.

**Malware marker.** Advisories with `isMalicious: true` render with a red/bold `MALWARE` column (optionally combined as `MALWARE · crit` when both flags exist). Count surfaces in the summary breakdown line as `N MALWARE · N crit · …`. Buckets partition every returned advisory: `MALWARE + crit + high + medium + low + unrated = advisories.length`, which equals `summary.total` when the upstream count and list stay consistent. Non-malicious advisories without a CVSS score bucket under `unrated` so the breakdown reconciles with the header total (common for PyPI / Rust advisories where CVSS may be absent).
**Malware marker.** Advisories with `isMalicious: true` render with a red/bold `MALWARE` column (optionally combined as `MALWARE | crit` when both flags exist). Count surfaces in the summary breakdown line as `N MALWARE | N crit | ...`. Buckets partition every returned advisory: `MALWARE + crit + high + medium + low + unrated = advisories.length`, which equals `summary.total` when the upstream count and list stay consistent. Non-malicious advisories without a CVSS score bucket under `unrated` so the breakdown reconciles with the header total (common for PyPI / Rust advisories where CVSS may be absent).

**Affected-range truncation (terminal-width aware).** The `affected` detail row under each advisory caps at 4 ranges on narrow terminals (≤119 cols), 6 on standard-wide (120–159 cols), and 8 on ultrawide (≥160 cols). The remainder collapses into a dim ` (+N more; use -v)` hint. Verbose mode (`-v`) shows every range. JSON output is never truncated — machine consumers get the full list.
**Affected-range truncation (terminal-width aware).** The `affected` detail row under each advisory caps at 4 ranges on narrow terminals (≤119 cols), 6 on standard-wide (120–159 cols), and 8 on ultrawide (≥160 cols). The remainder collapses into a dim `... (+N more; use -v)` hint. Verbose mode (`-v`) shows every range. JSON output is never truncated — machine consumers get the full list.

**Unrated severity column.** Advisories with no CVSS score (common on RUSTSEC / PYSEC upstreams) render with a dim `unrated` label in the severity column rather than an empty gutter, matching the header-breakdown vocabulary. They sort below banded advisories within the active bucket.

**Placeholder summary stripping.** When the upstream advisory feed returns the literal string `No summary available` (an OSV convention), both the JSON envelope and the terminal row drop the field entirely — absence of `summary` is the signal, and the advisory row is shorter as a result.

**Upgrade-path ordering.** `upgradePaths` are de-duplicated and sorted ascending by semver-ish comparison (pre-release suffixes rank below the matching base release), so the footer presents the minimum-churn upgrade first: `Upgrade options: 3.11.0, 4.0.0-rc1, 4.5.0, 4.19.2, …` rather than the backend's advisory-iteration order.

**Output envelope.** `{registry, name, version, requestedVersion?, summary: {total, affected?, bySeverity?}, advisories?, upgradePaths?}`. Each advisory: `{id?, aliases?, summary?, severity?, severityLabel?, affectedRanges?, fixedIn?, publishedAt?, modifiedAt?, withdrawnAt?, isMalicious?}`. `modifiedAt` included only when it differs from `publishedAt`. `isMalicious` included only when `true`.
**Output envelope.** `{registry, name, version, requestedVersion?, filter?, summary: {total, affected?, bySeverity?}, advisories?, upgradePaths?}`. `filter` echoes only explicit caller filters. Each advisory: `{id?, aliases?, summary?, severity?, severityLabel?, affectedRanges?, fixedIn?, publishedAt?, modifiedAt?, withdrawnAt?, isMalicious?}`. `modifiedAt` included only when it differs from `publishedAt`. `isMalicious` included only when `true`.

**Exit codes.** 0 on success including zero-vulns; 1 on any error. Under `--json`, the error envelope is written to **stderr**.

Expand Down
8 changes: 5 additions & 3 deletions docs/implementation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C
| `docs_list` | `registry`, `package_name`, `version?`, `limit?`, `after?`, `format?` | List hosted/crawled and repository-backed documentation pages for a package. Defaults to compact `text-v1` with ready-to-call `docs_read` follow-ups; repo-backed entries include exact source metadata for `code_read` follow-up when available. |
| `docs_read` | `page_id`, `start_line?`, `end_line?`, `format?` | Read a documentation page by page ID. Defaults to `text-v1` with a 150-line MCP text cap; explicit line ranges are supported. `format: "json"` preserves full-document default while still honoring explicit ranges. Repo-backed pages include exact file follow-up metadata. |
| `pkg_info` | `registry`, `package_name`, `verbose?`, `format?` | Latest-version package triage: license, description, repository popularity (stars/forks/issues and `[ARCHIVED]` when applicable), downloads, publish age, and vulnerability status. Example: `{registry:"npm", package_name:"express"}`. Set `verbose: true` for GitHub language/topics/last-pushed, recent advisories, and recent changes. Pass `format: "json"` for structured fields. |
| `pkg_vulns` | `registry`, `package_name`, `version?`, `min_severity?`, `include_withdrawn?`, `format?` | Known vulnerabilities for a package on npm, PyPI, Hex, or Crates. Defaults to compact summary text; pass `format: "json"` for per-advisory OSV ID + severity + affected/fix ranges, malware bucket, and upgrade paths. |
| `pkg_vulns` | `registry`, `package_name`, `version?`, `min_severity?`, `include_withdrawn?`, `verbose?`, `format?` | Known vulnerabilities for a package on npm, PyPI, Hex, or Crates. Example: `{registry:"npm", package_name:"lodash", version:"4.17.20", min_severity:"high"}`. Defaults to compact text capped at 5 advisory rows with active filter echo; set `verbose: true` to show all text rows or pass `format: "json"` for the complete per-advisory envelope. |
| `pkg_deps` | `registry`, `package_name`, `version?`, `lifecycle?`, `include_transitive?`, `include_importers?`, `max_depth?`, `format?` | Direct runtime dependency list by default with resolved versions. Non-runtime groups are hidden with an MCP-native hint (`pass lifecycle="all"`). Use `lifecycle: "runtime"` for explicit runtime-only, a concrete non-runtime lifecycle for runtime plus matching groups, or `lifecycle: "all"` for all available groups. Optional transitive output includes aggregate edge counts, the preprocessed install footprint, typed conflicts and circular-dependency cycles; opt into importer provenance with `include_importers`. Pass `format: "json"` for the lean structured envelope. |
| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?`, `format?` | Release notes or changelog entries for a package or GitHub repo. Defaults to compact text with newest-first entries and capped body previews; pass `format: "json"` for the structured envelope with full markdown bodies, or set `include_bodies: false` for a lean version / date / URL timeline. `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) is mutually exclusive. |
| `code_files` | `target`, `path?`, `path_prefix?`, `globs?`, `extensions?`, `file_types?`, `languages?`, `file_intent?`, `file_intents?`, `exclude_file_intents?`, `exclude_doc_files?`, `exclude_test_files?`, `include_hidden?`, `limit?`, `wait_timeout_ms?`, `format?` | List files in an indexed dependency. Returns `{total, hasMore, files: [{path, name, language, fileType, byteSize}], resolution, indexedVersion}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref` (repo). Selectors (`path`, `path_prefix`, `globs`) are OR-ed; the other filters intersect on top. Emits an `INDEXING` error envelope when the dependency is being indexed on-demand — retry with a longer `wait_timeout_ms` or pick a version from `details.availableVersions`. `format` defaults to `text-v1` (paths-only listing); pass `format: "json"` for the structured envelope. |
Expand All @@ -52,7 +52,9 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C

### `pkg_vulns` response shape

**Filter-aware summary.** `min_severity` and `include_withdrawn` are passed straight through to the service. The returned `vulnerabilityCount` reflects the filtered set — there is no client-side filtering and no `summary.filtered` dual-block. Callers wanting the unfiltered view omit the flag.
**Filter-aware summary.** `min_severity` and `include_withdrawn` are passed straight through to the service. The returned `vulnerabilityCount` reflects the filtered set — there is no client-side filtering and no `summary.filtered` dual-block. Callers wanting the unfiltered view omit the flag. Explicit filters are echoed as top-level `filter` in JSON (`{minSeverity?, includeWithdrawn?: true}`) and as `Filter` lines in text. Defaults and explicit `include_withdrawn:false` do not echo.

**Compact text vs verbose/JSON.** Default text caps the advisory list at 5 rendered rows and appends a surface-native hint (`use -v` on CLI, `use verbose=true or format=json` on MCP). Hidden-advisory counts are derived from the rendered advisory array, not backend summary counts. `--verbose` / `verbose:true` shows all advisory rows and full detail rows. JSON is never capped and ignores `verbose`.

**Partitioning buckets.** Advisories with `isMalicious: true` count **only** under `summary.bySeverity.malware`; severity bands (`critical`/`high`/`medium`/`low`) count non-malicious advisories with a positive CVSS score; non-malicious advisories with no score count under `summary.bySeverity.unrated`. Every returned advisory lands in exactly one bucket — the client-side guarantee is `MALWARE + crit + high + medium + low + unrated = advisories.length`. The sum also equals `summary.total`, which is re-derived from the deduped advisory list (see below). The malware bucket sorts to the top of the advisory list regardless of score. The `unrated` bucket ensures the terminal breakdown line reconciles with the header total on Rust / PyPI packages where a non-trivial fraction of advisories ship without a CVSS score.

Expand All @@ -66,7 +68,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C

**Registry coverage.** Only npm, PyPI, Hex, and Crates have vulnerability data. The CLI + MCP reject the other registries client-side with a tool-specific message (`pkg vulns only supports npm, pypi, hex, and crates. Got: ${registry}.`) — rejection predicate lives in `src/shared/package-vulnerabilities-request.ts` rather than the shared registry module, since it is a tool-specific capability matrix.

`pkg_vulns` shares its envelope builder and text formatter with the CLI `githits pkg vulns` command via `src/shared/package-vulnerabilities-request.ts` and `src/shared/package-vulnerabilities-response.ts`. MCP defaults to compact text and uses `format: "json"` for structured output. The parity test (`src/tools/package-vulnerabilities-parity.test.ts`) passes `format: "json"`, asserts `toEqual` across the service-sourced success and typed-error fixtures, and uses `toMatchObject` for builder-sourced `INVALID_ARGUMENT` fixtures such as unsupported registries and tag-style `v`-prefixed versions.
`pkg_vulns` shares its envelope builder and text formatter with the CLI `githits pkg vulns` command via `src/shared/package-vulnerabilities-request.ts` and `src/shared/package-vulnerabilities-response.ts`. MCP defaults to compact text and uses `format: "json"` for structured output. The shared text formatter is surface-aware so MCP hints never mention CLI flags. The parity test (`src/tools/package-vulnerabilities-parity.test.ts`) passes `format: "json"`, asserts `toEqual` across the service-sourced success/filter/typed-error fixtures, and uses `toMatchObject` for builder-sourced `INVALID_ARGUMENT` fixtures such as unsupported registries and tag-style `v`-prefixed versions.

### `pkg_deps` response shape

Expand Down
6 changes: 6 additions & 0 deletions eval/agentic/workloads/package-vulnerability-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Workload: Package Vulnerability Filter Check

You are evaluating whether `npm:lodash@4.17.20` is safe to keep pinned in a
production Node.js service. Summarize the active vulnerability risk for that
specific version, focusing on high-or-worse advisories. Include whether there
are fix versions or upgrade guidance, and call out any caveats in the evidence.
27 changes: 27 additions & 0 deletions scripts/cli-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,15 @@ async function runLiveSmoke(): Promise<void> {
"pkg vulns terminal missing context",
);

const filteredVulnsText = assertTerminalOutput(
await runCli(["pkg", "vulns", "npm:lodash@4.17.20", "--severity", "high"]),
"pkg vulns filtered terminal",
);
assert(
filteredVulnsText.includes("Filter severity >= high"),
"pkg vulns filtered terminal missing filter echo",
);

const vulnsJson = assertJsonOutput(
await runCli(["pkg", "vulns", "npm:express", "--json"]),
"pkg vulns json",
Expand All @@ -477,6 +486,24 @@ async function runLiveSmoke(): Promise<void> {
"pkg vulns json missing vulnerability data",
);

const filteredVulnsJson = assertJsonOutput(
await runCli([
"pkg",
"vulns",
"npm:lodash@4.17.20",
"--severity",
"high",
"--json",
]),
"pkg vulns filtered json",
);
assertRecord(filteredVulnsJson, "pkg vulns filtered json");
assertRecord(filteredVulnsJson.filter, "pkg vulns filtered json filter");
assert(
filteredVulnsJson.filter.minSeverity === "high",
"pkg vulns filtered json missing severity filter echo",
);

const changelogText = assertTerminalOutput(
await runCli(["pkg", "changelog", "npm:express", "--limit", "1"]),
"pkg changelog terminal",
Expand Down
36 changes: 36 additions & 0 deletions scripts/mcp-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,25 @@ async function runLiveSmoke(client: Client): Promise<void> {
vulnsText.includes("express") || vulnsText.includes("vulnerab"),
"pkg_vulns default missing context",
);
assert(!vulnsText.includes("use -v"), "pkg_vulns leaked CLI -v hint");

const filteredVulnsText = assertDefaultText(
await callTool(client, "pkg_vulns", {
registry: "npm",
package_name: "lodash",
version: "4.17.20",
min_severity: "high",
}),
"pkg_vulns filtered default",
);
assert(
filteredVulnsText.includes("Filter severity >= high"),
"pkg_vulns filtered default missing filter echo",
);
assert(
!filteredVulnsText.includes("use -v"),
"pkg_vulns filtered default leaked CLI -v hint",
);

const vulnsJson = assertJsonResult(
await callTool(client, "pkg_vulns", {
Expand All @@ -376,6 +395,23 @@ async function runLiveSmoke(client: Client): Promise<void> {
"pkg_vulns json missing vulnerability data",
);

const filteredVulnsJson = assertJsonResult(
await callTool(client, "pkg_vulns", {
registry: "npm",
package_name: "lodash",
version: "4.17.20",
min_severity: "high",
format: "json",
}),
"pkg_vulns filtered json",
);
assertRecord(filteredVulnsJson, "pkg_vulns filtered json");
assertRecord(filteredVulnsJson.filter, "pkg_vulns filtered json filter");
assert(
filteredVulnsJson.filter.minSeverity === "high",
"pkg_vulns filtered json missing severity filter echo",
);

const changelogText = assertDefaultText(
await callTool(client, "pkg_changelog", {
registry: "npm",
Expand Down
Loading
Loading