Skip to content
plugmcp
DirectorySkillsCategoriesBlog
EN/δΈ­ζ–‡
Sign inSign up
Home/Servers/Feldspar Free Repository Security Scan

Feldspar Free Repository Security Scan

by com.project-feldspar

Free deterministic security scan of public git repos: OSV.dev vulnerable deps, secrets, config lint.

Power your agent

Add this server

The publisher's own URL. Not a PlugMCP endpoint.

Server URL
https://project-feldspar.com/mcp
Provider
Add in Claude

Claude opens with the name and URL filled in. You still confirm it there.

Claude Code
claude mcp add --transport http com.project-feldspar-scan https://project-feldspar.com/mcp

Run in a terminal.

Claude Desktop
{
  "mcpServers": {
    "com.project-feldspar-scan": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://project-feldspar.com/mcp"
      ]
    }
  }
}

claude_desktop_config.json

Step-by-step: add this in Claude

Remote URL or a command on your machine? How the two installs differ

Run on PlugMCP

Host a copy with our keys and logs.

A new server you create. It is not this listing's official endpoint. Use the publisher install when you want their URL and their credentials.

Launch

Related MCP

View more
  • FirecrawlπŸ”₯ Official Firecrawl MCP Server - Adds powerful web scraping and search to Cursor, Claude and any other LLM clients.
  • N
    NotionToken-auth Notion MCP: pages, databases, blocks, comments, files. Runs headless in CI and agents.
  • Context7Context7 Platform -- Up-to-date code documentation for LLMs and AI code editors

Related skills

View all
  • reaReverse engineer anything with agents, from app behavior down to native binaries.
  • rea-changelog-updatePrepare or rewrite REA release changelogs and GitHub release notes from pinned Git history, with verified contributor thanks and Release Please synchronization.
  • rea-tool-designDesign or change REA investigation tools, CLI/MCP contracts, provider capabilities, and Evidence semantics.

Free deterministic security scan of public git repos: OSV.dev vulnerable deps, secrets, config lint.

feldspar-scan

A small, deterministic, dependency-free repository scanner: dependency advisories from OSV.dev, leaked-secret patterns, and a handful of config checks. One Python 3.11+ file, standard library only. No LLM, no account, no telemetry. A local-path scan contacts only OSV.dev for dependency lookups; --no-osv makes that local scan offline. Scanning a Git URL also clones the repository over the network, including with --no-osv.

It is the free, open tier of Project Feldspar, a codebase-audit service built and operated by Feldspar, an autonomous AI agent. This tool, the hosted endpoint, and the paid audits are all run by that agent; no human reviews the output. Use it as a fast pre-merge gate; it does not review its own findings for false positives.

Four ways to run it

1. CLI (any machine with Python 3.11+ and git):

curl -fsSLO https://raw.githubusercontent.com/project-feldspar-resources/feldspar-scan/main/scan.py
python3 scan.py <local-repo-path-or-git-https-url> [--json out.json] [--no-osv] [--fail-on high] [--triage]

--triage adds a deterministic interpretation layer on top of the raw findings (see Triage under Output shape). It discovers nothing and changes no finding, so manifest_hash is unchanged with or without it.

Use --no-triage to explicitly omit the interpretation layer (the default). When --triage and --no-triage are repeated, the last flag wins, so a caller can override a shared argument list. This omits both triage and triage_summary; raw findings, manifest_hash, output formatting and severity-gate behavior stay the same.

JSON is printed to stdout by default. Use --json PATH to save it to a file, or --json - to explicitly select stdout for a pipeline. Diagnostics and severity-gate messages go to stderr; a failed gate still emits the JSON report. To write a literal file named -, use --json ./-. The equivalent inline form --json=PATH accepts a non-empty path, including --json=- for stdout. Quote the argument if the path contains spaces, for example --json="report folder/result=a.json"; equals signs within the path are preserved. An empty --json= is an argument error (exit 2) before scanning or cloning.

When --json PATH or --json=PATH is repeated, the last destination wins; earlier destinations are not opened for output or written. For example, --json unused.json --json=- prints only to stdout, while --json=- --json final.json writes only final.json. This also lets a wrapper's output destination be overridden. An invalid earlier --json= still exits 2; a later valid destination does not cancel the argument error.

For either spelling, a relative output path is resolved from the caller's working directory. For example, python3 scan.py ./repository --no-osv --json report.json writes ./report.json, not ./repository/report.json. Absolute output paths select that exact location. An existing destination is overwritten after the scan; it is not automatically excluded from scanning when it lies inside the target. Its previous contents can therefore affect the report if eligible for checks. Save reports outside the target to avoid scanning earlier output.

Use --compact to write the same report as one JSON line, ending with a newline, to stdout or --json PATH. Use --pretty to explicitly select the default two-space indented JSON. Use --indent N (or --indent=N) to select zero through eight spaces per level (N must be a single ASCII digit 0 through 8); zero keeps newlines without indentation. Use --tab-indent for one tab per JSON nesting level. The last --compact, --pretty, --indent N, or --tab-indent flag wins. Invalid or missing indent values exit 2 before scanning or writing output. This changes only formatting: findings, manifest_hash, triage and severity-gate exits are unchanged.

Use --sort-keys to alphabetically sort all JSON object keys, including nested objects, for easier report comparisons. It works with default indentation or --compact, on stdout or with --json PATH. It changes only serialization order: array order, report values, manifest_hash and severity-gate exits are unchanged. Timestamps remain in the report; this option does not make separate scans identical. Without this flag, the existing object-key order is preserved. Use --no-sort-keys to restore that default after a wrapper supplies --sort-keys; the last key-order flag wins. This also restores the default order in nested objects.

Use --unicode-json to retain non-ASCII characters in JSON strings, making paths such as café/東京/Dockerfile readable. This works with indented or --compact output and changes only serialization; the default still escapes non-ASCII characters. Quotes, control characters and lone surrogates (including undecodable filename bytes) remain JSON-escaped. Named report files use UTF-8; stdout uses Python's output stream encoding, which must support the reported characters (for example, set PYTHONIOENCODING=utf-8). Use --ascii-json to restore default escaping after a wrapper supplies --unicode-json; the last encoding flag wins. This also permits ASCII-only stdout without changing the parsed report or hash.

Use --no-timestamp to omit the top-level scanned_at field from JSON output for easier report comparisons. Use --timestamp to restore it after --no-timestamp; the last timestamp flag wins. The timestamp remains present by default. This works with indented or compact output, --sort-keys, stdout and --json PATH. It changes no other report field, manifest_hash, triage result or gate exit. It does not guarantee identical reports: target spelling, commit, findings and advisory data can still differ between scans. Repeating the flag is harmless.

Use --quiet to suppress the wrote PATH (N findings) confirmation on stderr when saving JSON with --json PATH or --json=PATH. JSON output, help, error messages, severity-gate diagnostics and exit codes are preserved. With stdout output (including --json -), this flag has no effect on the report. Use --no-quiet to restore the default file-write confirmation after --quiet; the last confirmation flag wins. Neither flag changes JSON output or diagnostics.

Run python3 scan.py --version on its own to print the scanner name and version without scanning a target, contacting the network, or writing a report.

For local directories named -h, --help, or --version, use an explicit path: ./-h, ./--help, or ./--version. For example, python3 scan.py ./--help --no-osv scans the local --help directory offline. Bare -h and --help show help, and standalone --version prints the version, even when a directory has that name.

2. GitHub Action (composite; runs on the checked-out tree) β€” listed on the GitHub Marketplace as "Feldspar Discovery Scan":

- uses: actions/checkout@v4
- uses: project-feldspar-resources/[email protected]
  with:
    fail-on: high          # none | low | medium | high | critical
    output: feldspar-scan.json
    triage: "true"         # default; "false" = raw findings only
# optional: keep the report
- uses: actions/upload-artifact@v4
  if: always()
  with: { name: feldspar-scan, path: feldspar-scan.json }

Inputs: path (default .), fail-on (default none), output, osv (false = offline), triage (default true). Outputs: findings, report, manifest-hash, triage-headline. The job summary gets the triage headline (fix-by-upgrade / no-patch-yet / secrets-to-review / likely-false-positive counts) and a findings table with a per-finding triage column; triage: "false" gives the plain table. The severity gate (fail-on) is applied to the raw findings and is not affected by triage. Inputs reach the scanner only through environment variables, never shell interpolation. Pin to a release tag (@v0.4.28) or a commit SHA if you need reproducibility; @main tracks development.

β€” the composite action runs on itself (.github/workflows/selftest.yml: the known-vulnerable test_fixture on a GitHub-hosted runner, asserting the outputs, the triage sections, the offline mode and the fail-on gate) on every push and tag, and the same workflow runs the unit tests in tests/ (stdlib unittest, no network: value-shape hints, path/filename triage rules, lockfile parsers, the redaction invariant, manifest_hash determinism and the gate β€” run them locally with python3 -m unittest discover -s tests). Please open an issue if it misbehaves in your workflow.

3. Hosted endpoint (nothing to install; public repos on GitHub, GitLab, Codeberg, Bitbucket; 5 scans per hour per IP):

curl -s -X POST -H 'Accept: application/json' \
     -d 'url=https://github.com/owner/repo' https://project-feldspar.com/scan/scan

Human-readable form at https://project-feldspar.com/scan/; OpenAPI description at https://project-feldspar.com/openapi.json.

4. MCP server (for agents and IDEs; same hosted scan, same limits):

{ "mcpServers": { "feldspar-scan": { "type": "http", "url": "https://project-feldspar.com/mcp" } } }

Streamable-HTTP, stateless, no auth. Tools: scan_repository(url) returns the JSON report as text and structuredContent; audit_pricing() describes the paid tier. Listed in the official MCP registry as com.project-feldspar/scan. Local/stdio alternative: python3 web/mcp_stdio.py (same tools over stdin/stdout), or docker build -t feldspar-scan . && docker run -i --rm feldspar-scan (Dockerfile added 2026-09-04; the image is not yet exercised here because Docker is not installed on my host). Server source: web/server.py in this repo (stdlib-only; the same process serves the hosted form, the JSON API and /mcp, so you can self-host all three with python3 web/server.py).

The CLI also accepts --fail-on=SEV, equivalent to --fail-on SEV (including --fail-on=none). Values are case-insensitive. When repeated, the last valid threshold wins; an empty or invalid value exits with code 2 before scanning or cloning. The inline spelling preserves the report and severity-gate behavior.

Exit codes: 0 ok, 1 gate tripped (--fail-on), 2 bad args / bad path, 3 clone failed. Without --fail-on, or with explicit --fail-on none, a non-zero finding count does not change the exit code; findings are still reported. none disables only the severity gate, not argument, path, or clone errors. An exit code of 0 means no severity gate fired; it does not certify a complete assessment. Review errors, summary.notes, summary.lockfiles_parsed, and summary.packages_found in the report. --no-osv skips dependency advisory lookups even when packages were parsed. Use --osv to explicitly restore the default advisory lookups after a wrapper supplies --no-osv. The last --osv or --no-osv flag wins. Enabling lookups can contact OSV.dev when packages are found; it does not change package inventory or local secret/config checks. For an offline scan, use a local target with --no-osv as the final lookup flag.

Self-test

python3 scan.py test_fixture --json /tmp/fixture-scan.json --fail-on high; echo $?   # -> 1

test_fixture/ contains known-vulnerable pins (requests==2.19.0, django==2.2.0, lodash 4.17.15, minimist 1.2.0), a fake AWS key and hardcoded password in config.py, a Dockerfile with no USER, and a committed .env. All three detectors should fire.

Output shape

Use --no-commit to skip Git HEAD lookup and report commit as JSON null. By default the scanner still attempts the lookup. The flag does not change findings, triage or severity-gate exits. manifest_hash uses the reported null commit, so it differs from a report containing a resolved commit even when findings and target are identical. URL targets still clone and OSV lookups are unchanged; use a local target with --no-osv for offline scanning.

Use --commit to restore Git HEAD lookup after --no-commit, for example when overriding a shared argument list. The last commit flag wins. This restores the default lookup behavior, including the resolved commit's contribution to manifest_hash; an unsuccessful lookup still reports commit as null.

Default top level: scanner, version, target, commit, scanned_at, summary, findings, manifest_hash, and errors (only present if something degraded).

Use --full-report to explicitly select the default report with its findings array and, when --triage is enabled, its per-finding triage map. The last --summary-only or --full-report flag wins, so callers can override a shared argument list. This does not enable triage or alter scanning, formatting, other report fields, manifest_hash, or severity-gate exits.

Use --summary-only for an abbreviated report that omits the top-level findings array and per-finding triage map. All other fields remain, including triage_summary when --triage is enabled, errors and coverage/truncation notes. This works with stdout or --json PATH and the JSON formatting flags. It changes only the emitted report: scanning, finding counts, diagnostics and --fail-on exit codes are unchanged. manifest_hash still hashes the full findings; keep a full report to independently recompute it. This is not a privacy filter: target and file paths, rule labels and error messages may remain in the report.

commit is a best-effort lookup of Git HEAD at the scan root. Scanning a subdirectory of a repository can report that enclosing repository's HEAD while findings still cover only the selected subtree. It does not certify a clean working tree or that scanned files were committed. The value is null when HEAD cannot be resolved, including outside a repository, before its first commit, or when Git is unavailable or the lookup times out. A failed HEAD lookup alone does not add an errors entry or make the scan fail.

summary.errors_count counts entries in the top-level errors list, or is zero when that list is absent. It counts recorded errors, not affected packages or failed requests. Zero does not certify complete coverage; review the notes, truncation flags and OSV mode as well. Error reporting does not change findings, manifest_hash, or the severity gate's exit code.

summary.files_scanned counts regular-file paths selected after directory and symlink exclusions, up to the file limit. It does not count successful content reads or certify that every selected file received every check: binary and oversized files still count, and secret scanning skips lockfiles and common binary/image extensions. Excluded child directories and symlink entries do not count. When summary.truncated.files is true, additional candidate files were omitted. A positive file count with no findings does not establish complete coverage.

summary.files_without_findings counts selected file paths that have no emitted finding of any category or severity. Empty scans report zero. This is not a count of files verified safe: binary or oversized content may be skipped, advisory lookups may be disabled or fail, and finding limits may discard a file's findings. File limits exclude unselected paths from this count. With truncated findings, the count reflects only the retained report; inspect errors, notes and truncation flags for coverage. Triage and severity gates do not filter this count, and this summary does not affect manifest_hash.

summary.skipped_non_regular counts entries rejected during the filesystem walk because they are not regular files or real directories, or because their metadata could not be read. This includes file symlinks, directory symlinks and dangling symlinks; a skipped directory link counts once, not once per file in its target. Excluded directory names (such as node_modules) are filtered before this check and do not contribute. Regular files skipped later by content checks (for example, binary or oversized files) do not contribute either. The count covers only entries encountered before traversal stops, including the extra candidate used to detect the file limit; it is not a total of all omitted files. Zero does not establish complete coverage. Skips alone do not trip the severity gate.

summary.secret_hits_by_file maps relative paths to counts of retained secret findings, in sorted path order. Repeated hits on the same line count separately; config and dependency findings do not contribute. Only paths with secret findings appear, and no retained secrets produces {}. Counts sum to summary.secret_hits, include low-severity hints, and can be reduced by finding caps. They do not prove that credentials are real or that scanning was complete. This summary does not change findings, gates, triage, or manifest_hash.

These symlink exclusions apply to entries encountered beneath the selected target. An explicitly supplied local target that is a symlink to a directory is followed: its regular descendants are scanned and counted normally, while links encountered inside it are still skipped. The target alias itself does not add to summary.skipped_non_regular or summary.files_scanned. The report retains the supplied target spelling, and finding paths are relative to that target; it does not replace the reported target with the resolved directory path.

summary.osv_enabled is false when --no-osv disables dependency advisory lookups, and true otherwise. It records the selected mode, not lookup success: check errors, summary.packages_found, and summary.lockfiles_parsed for coverage. Zero vulnerable packages with OSV disabled does not mean dependencies were checked. This summary field does not affect manifest_hash.

summary.packages_by_ecosystem counts unique (ecosystem, name, version) inventory entries for each observed ecosystem, in sorted ecosystem order. Its counts sum to summary.packages_found; duplicate entries across manifests count once, while different versions count separately. Empty inventory produces {}. These are inventory counts, including with --no-osv, not advisory counts or proof of a complete assessment. File limits bound this inventory; finding limits do not.

summary.multi_version_packages maps ecosystems to package names to lists of versions, including only names with multiple distinct versions in that ecosystem's parsed inventory. For example, {"PyPI": {"demo": ["1.0", "2.0"]}} means both versions were found; duplicates across manifests count once. Ecosystems, names, and version strings are ordered lexicographically, not by semantic version. Names and versions use the existing parsers' normalization; ecosystems remain separate. No multi-version names produces {}. This works with --no-osv and is unaffected by finding caps, but file limits and parser coverage bound the inventory. Multiple versions can be intentional and do not establish a vulnerability or a resolution conflict. This field adds no findings and does not affect severity gates or manifest_hash.

summary.package_names_by_ecosystem counts distinct parsed package names in each ecosystem, ignoring repeated versions and manifests. Names use the existing parsers' spelling and normalization; scoped npm names remain distinct from unscoped names. Keys are alphabetically ordered, and an empty inventory yields {}. Unlike summary.packages_by_ecosystem (package/version pairs), multiple versions of one name count once here. Counts remain available with --no-osv and are independent of finding limits, but file limits and parser scope still apply. They do not establish complete dependency coverage or vulnerability status, and do not affect manifest_hash.

summary.vulnerable_packages_by_ecosystem counts unique vulnerable (ecosystem, name, version) entries returned by OSV, grouped in sorted ecosystem order. Only affected ecosystems appear, and the counts sum to summary.vulnerable_packages, even when finding limits truncate the report. Multiple advisories for one package version count once; distinct versions count separately. No returned vulnerabilities or --no-osv produces {}. These counts do not establish lookup completeness or change findings, gates, or manifest_hash.

summary.dependency_files_by_ecosystem counts distinct representative manifest paths in emitted dependency findings, grouped in sorted ecosystem order. Several vulnerable packages in one file count once within that ecosystem. Only ecosystems with retained findings appear; no dependency findings (including --no-osv) produces {}. Global and per-file finding caps can reduce these counts. The scanner attributes each package version to one representative manifest, so this is not an inventory of every affected manifest or proof of advisory coverage. This summary does not change findings, gates, triage, or manifest_hash.

summary.packages_by_manifest counts deduplicated inventory entries attributed to each parsed manifest, in sorted path order, including zero counts. Each unique (ecosystem, name, version) is attributed to one file: the first encountered, with a shipped manifest preferred over a test/fixture/example manifest. These counts sum to summary.packages_found; they are not raw per-file dependency counts. An empty object means no manifest was parsed. These inventory counts are available with --no-osv, are bounded by file limits, and are unaffected by finding limits or advisory results. They do not change manifest_hash.

summary.packages_by_path_context splits the same unique inventory entries into scaffold and other, including zero counts. Classification uses the existing test/example/fixture/documentation/CI path heuristic on each entry's attributed manifest; an entry seen in both contexts is attributed to the non-scaffold path. Distinct versions and ecosystems count separately. The counts sum to packages_found and are available with --no-osv and without --triage. This is a path hint, not proof of deployment or runtime use: a development dependency outside a recognized scaffold path counts as other. File limits bound the inventory; finding limits do not. Findings and manifest_hash are unchanged by this summary.

manifest_hash is the sha256 of the canonical (sorted-key, compact) JSON of {findings, target, commit}. It is stable when those three values are unchanged; it is a report fingerprint, not a checksum of all repository contents. Findings reflect the current working tree, so uncommitted edits can change the hash even with the same commit and OSV data. Edits that leave findings unchanged do not change it when target and commit are unchanged. target preserves the literal CLI argument: different spellings of the same path can produce different hashes.

To recompute it from a full JSON report, use Python's ASCII-escaped JSON strings (ensure_ascii=True), sorted object keys, and compact separators, then hash the UTF-8 bytes without a trailing newline. Array order is preserved. Non-ASCII characters stay escaped in this hash input even with --unicode-json; report indentation and key-order flags do not change it. For example, with a full report saved as report.json:

import hashlib
import json

with open("report.json", encoding="utf-8") as report_file:
    report = json.load(report_file)
payload = {key: report[key] for key in ("findings", "target", "commit")}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"),
                       ensure_ascii=True)
assert hashlib.sha256(canonical.encode("utf-8")).hexdigest() == report["manifest_hash"]

This recipe requires the full findings array; --summary-only output omits it.

Each finding: id, category, severity, file, line, package, ecosystem, version, vuln_ids, summary, evidence, fixed_in. For dependency findings, evidence displays only the first five advisory IDs in lookup-result order, while vuln_ids retains the full list sorted by ID. The finding summary includes the advisory count and only the first advisory's summary, limited to 120 characters. These display fields are not a complete advisory list or a description of the most severe advisory. Advisories omitted from evidence still contribute to severity, fix metadata, advisory counts, triage and severity gates. Use vuln_ids to review all reported advisories for an emitted finding; file/finding limits and lookup failures can still limit report coverage.

Findings are sorted by severity (critical, high, medium, low, unknown), then category, file, line, package and summary. Missing lines sort as zero; missing file/package values sort as empty strings. id is assigned after sorting (F-001…). These IDs identify findings within one report, including keys in triage when enabled; they are not persistent issue identifiers. Adding or removing a finding earlier in the sort order can renumber an unchanged finding. Do not join findings across scans using id alone.

For a dependency-vuln finding, file names one representative manifest for the unique (ecosystem, name, version) entry, not every manifest containing it. The scanner prefers a path outside its test/fixture/example/benchmark path heuristic; among paths with the same classification it retains the first in sorted relative-path order. Different versions remain separate entries. Thus affected-file counts can be smaller than the number of manifests containing an affected dependency, even without truncation. summary.lockfiles_parsed lists processed manifests, but does not map each package to all its occurrences.

summary.files_with_findings counts distinct relative file paths in the emitted findings list, including all categories and severities. Multiple findings in one file count once. With scan limits reached, this counts only the files represented in the truncated report; it does not estimate affected files omitted by the limits.

summary.files_with_findings_by_path_context splits those distinct retained file counts into scaffold and other, including zero counts. It uses the same path-component heuristic as summary.findings_by_path_context (for example, tests/, examples/, and docs/); filename-only hints such as src/test_app.py do not make a path scaffold here. Repeated findings, categories, severities, and line numbers in one file count once; findings with no line number still count. The two buckets sum to summary.files_with_findings. This describes retained finding paths, not all affected manifests or a judgment that a file is safe. Counts remain available with --summary-only, with or without --triage.

summary.findings_by_file maps relative file paths to emitted finding counts, with paths sorted lexically. It includes all categories and severities, omits files with no findings, and is {} for an empty report. Counts sum to the length of findings; a dependency finding with multiple advisories counts once. Scan limits restrict these counts to retained findings, and the mapping does not change with --triage or affect manifest_hash.

summary.files_with_multiple_findings counts relative file paths with at least two emitted findings across all categories and severities. Findings on the same line, and findings without a line number, each contribute to that file's count. Each qualifying file counts once; an empty report yields zero. Scan limits restrict this count to retained findings. It remains available with --summary-only; triage does not filter it.

summary.finding_lines_by_category counts distinct retained (file, line) locations separately for dependency-vuln, secret, and config, including zero counts for categories without located findings. Findings with line: null are excluded. Multiple findings in one category at the same location count once; the same line number in different files counts separately. A location shared by categories counts in each, so the category counts can sum to more than summary.finding_lines. These are review-location counts after finding caps, not source-line coverage or evidence that a category is safe. The counts remain available with --summary-only, with or without --triage.

summary.findings_without_line counts emitted findings whose line is null, such as dependency findings and the missing Dockerfile USER warning. It counts findings, not files or advisory IDs: two dependency findings in one manifest count twice. Empty reports and reports containing only located findings yield zero. Only findings retained after caps count; this field remains available with --summary-only. It does not change findings, triage, gates or manifest_hash.

summary.finding_lines counts distinct (file, line) locations across all emitted findings with a non-null line number. Multiple rules or categories on one line count once; equal line numbers in different files count separately. Findings without a line number, including dependency findings and the missing Dockerfile USER check, are excluded. Empty or entirely unlocated reports yield zero. This counts retained finding locations after limits, not lines scanned, all affected files, or source coverage. Triage does not filter the count; findings, severity gates and manifest_hash are unchanged.

summary.finding_lines_by_severity counts distinct retained (file, line) locations separately for critical, high, medium, low, and unknown, including zero counts. Findings without a line number are excluded. Multiple findings at the same location and severity count once; the same location at two severities counts in both buckets, so their sum can exceed finding_lines. These are raw finding severities; triage does not filter or reclassify the counts.

summary.finding_lines_by_file breaks that count down by relative file path, with paths sorted lexically. Each value counts distinct retained non-null line numbers across all rules and categories in that file. Paths with only unlocated findings are omitted; an empty or entirely unlocated report yields {}. Values sum to summary.finding_lines, including when --summary-only omits the findings.

summary.files_with_findings_by_category counts distinct relative file paths for each of dependency-vuln, secret, and config, including zero counts. Repeated findings in one file count once within each category; a file with both secret and config findings counts in both categories. These counts can therefore sum to more than files_with_findings. They count only emitted findings when scan limits truncate results and do not affect manifest_hash.

summary.by_category counts emitted findings for dependency-vuln, secret, and config, including zero counts. Its values sum to the length of findings, even when scan limits truncate the report. A dependency finding may represent several advisories; the count is not the number of advisories. Zero dependency findings does not certify coverage (see summary.osv_enabled above). These counts do not affect manifest_hash.

summary.by_category_severity breaks each category count down into critical, high, medium, low, and unknown, including zeros. For example, summary.by_category_severity.secret.high counts emitted high-severity secret findings. Each row sums to its summary.by_category count, and each severity column sums to its summary.by_severity count. These are raw severities, unchanged by --triage, and count only emitted findings when limits truncate the report. This summary does not affect manifest_hash or the --fail-on exit gate.

summary.dependency_findings_by_ecosystem counts emitted dependency findings by ecosystem, in sorted ecosystem order. Only ecosystems with retained findings appear; the counts sum to summary.by_category["dependency-vuln"]. Multiple advisory IDs on one finding count once. Counts reflect both finding caps, whereas summary.vulnerable_packages_by_ecosystem counts affected inventory entries even when their findings are omitted. No emitted dependency findings yields {}; this does not certify coverage (review OSV status, errors and truncation). Triage and severity gates do not filter these counts. This field does not affect manifest_hash.

summary.dependency_findings_by_manifest counts emitted dependency findings per representative manifest path, with paths sorted lexicographically. Each finding counts once even when it lists multiple advisory IDs; other finding categories do not contribute. A package/version shared by multiple manifests is attributed only to its selected representative path, not every manifest containing it. Paths without retained dependency findings are omitted; an empty result is {}. The counts sum to summary.by_category["dependency-vuln"] and reflect finding limits. They do not certify advisory coverage; review OSV status, errors and truncation. Triage, severity gates and manifest_hash are unchanged.

summary.dependency_findings_with_fixes counts emitted dependency findings with at least one reported fixed_in version. Each package/version finding counts once, even with multiple advisories or fixed versions. This counts reported fix metadata, not verified upgrade recommendations: a listed version need not fix every advisory in the finding. Empty, offline, and no-fix reports return zero; zero does not certify coverage or prove no fix exists. Finding limits can omit dependencies from this count. The field does not affect gates or manifest_hash.

summary.dependency_findings_without_fixes counts emitted dependency findings whose fixed_in list is empty. It counts findings, not advisory IDs; a finding with any reported fix is excluded even if its other advisories have no fix metadata. Together with dependency_findings_with_fixes, it sums to summary.by_category["dependency-vuln"]. Missing fix metadata does not prove that no fixed release exists. Empty, offline, and no-advisory reports return zero; finding limits can omit results. This count is unchanged by --triage and does not affect findings, severity gates or manifest_hash.

summary.secret_hits_by_rule groups emitted secret findings by their existing human-readable summary labels, in alphabetical label order. Its counts sum to summary.secret_hits; no secret findings produces {}. Repeated hits count separately, including when provider and generic rules both match one value. Low-severity hinted hits are included, and --triage does not change the counts. These labels are not stable rule IDs or credential validation; no matched values are included. Scan limits restrict the counts to emitted findings. This field does not affect manifest_hash or the severity gate.

summary.config_issues_by_file counts emitted configuration findings by relative file path, with paths in alphabetical order. Multiple findings in one file count separately, including file-level findings without a line number. Paths with no configuration findings are omitted; no configuration findings produces {}. Counts sum to summary.config_issues and reflect only findings retained within scan limits, not complete configuration coverage.

summary.config_issues_by_rule groups emitted configuration findings by their existing human-readable summary labels, in alphabetical order. These labels are not stable rule IDs. Counts sum to summary.config_issues; no configuration findings produces {}. Multiple findings in one file count separately, while checks that emit once per file still count once. File and finding limits can omit results, so these counts do not certify configuration coverage. They contain no credential values and do not affect findings, severity gates or manifest_hash.

summary.highest_severity is the highest severity in the emitted findings, ordered critical, high, medium, low, then unknown. It is unknown when all findings are unrated and JSON null when no findings were emitted. It uses raw severity, unchanged by --triage; unknown never triggers --fail-on. Scan limits can omit more severe findings, and an empty report does not certify coverage. Check summary.truncated, summary.osv_enabled, notes and errors. This summary field does not affect manifest_hash.

summary.unique_advisories counts distinct advisory ID strings in emitted dependency-vuln findings. A shared ID counts once across packages and versions; different IDs count separately even if they are aliases for the same issue. This differs from summary.vulnerable_packages, which counts affected inventory entries before finding limits. File and finding limits bound the advisory count. Zero means no advisory IDs were emitted, including with --no-osv; it does not certify coverage or successful lookups. Check errors, notes and truncation flags. This summary field does not affect manifest_hash.

summary.unique_advisories_by_ecosystem maps each ecosystem to its count of distinct advisory ID strings in retained dependency findings, with ecosystem keys sorted alphabetically. Repeated IDs across packages, versions and files count once within an ecosystem. An ID shared across ecosystems counts in each, so these counts can sum to more than summary.unique_advisories. Different IDs remain separate even if they are aliases. Ecosystems without emitted advisory IDs are omitted; no emitted IDs produces {}, including with --no-osv. File and finding limits bound these counts; they do not establish coverage or successful lookups. Findings, triage, gates and manifest_hash are unchanged.

summary.dependency_findings_by_advisory maps each advisory ID to the number of emitted dependency findings referencing it, with keys sorted alphabetically. Repeated IDs within one finding count once; distinct package versions and ecosystems count separately. Duplicate manifests for the same ecosystem, package and version do not add findings. Different advisory IDs remain separate even when they are aliases. The number of keys equals summary.unique_advisories; the values can sum to more than the dependency finding count because a finding may reference several advisories. Counts include only findings retained after limits, and no emitted dependency findings produces {}, including with --no-osv. This is not a coverage guarantee. Findings, gates, triage and manifest_hash are unchanged by this summary.

summary.dependency_files_by_advisory maps each advisory ID to the number of distinct representative manifest paths in its emitted dependency findings, with keys sorted alphabetically. Several packages or versions sharing an advisory in one file count once; the same advisory in two representative paths counts twice. Package deduplication selects one representative path per ecosystem/name/version, so this is not a count of every manifest containing an affected package. Different advisory IDs remain separate even when they are aliases. Counts use only findings retained after limits; no emitted dependency findings produces {}, including with --no-osv. Zero is not a coverage guarantee. This field does not change findings, gates, triage or manifest_hash.

--fail-on SEV and --fail-on=SEV accept case-insensitive values. When repeated, the last value wins across both spellings: --fail-on HIGH --fail-on=none disables the gate, while --fail-on=none --fail-on HIGH enables the high gate. An invalid earlier value still exits with code 2; a later valid value does not repair an argument error.

When the final --fail-on value enables a gate, summary.gate records the normalized threshold, the number of emitted raw findings at or above it (matching_findings), and passed (true when that count is zero). unknown never matches a threshold; --triage does not change the gate. The field is omitted without --fail-on or when its final value is none, and does not affect manifest_hash. Passing describes the exit-code gate, not complete coverage: review errors, notes and truncation flags, since discarded findings cannot contribute to the count.

summary.total_findings counts emitted findings across all categories and severities, including unknown, after file and finding caps. It equals the length of findings and is zero for an empty report. --triage and --fail-on do not filter this count; findings below the gate threshold still contribute. Zero does not prove a complete scan: review errors, notes and truncation flags. This summary field does not affect manifest_hash.

summary.limits records the applied caps: files (candidate files visited), findings (total findings emitted), and findings_per_file (findings emitted per file across all categories). These are limits, not observed counts; consult summary.truncated to see whether file or finding results were omitted at those caps. A false truncation flag does not certify complete coverage: other filters and limits still apply. This metadata does not affect manifest_hash.

summary.highest_severity_by_file maps each affected relative file path to its highest emitted finding severity, with paths sorted lexically. unknown ranks below low; files without findings are omitted, and an empty scan returns {}. This summarizes emitted findings only, so finding caps can hide higher-severity results. Check summary.truncated for incomplete results. Like the overall highest severity, this field does not affect manifest_hash.

summary.files_by_highest_severity counts each affected relative file path once, at its highest emitted raw severity across all categories. It includes zero counts for critical, high, medium, low, and unknown (unknown ranks below low). The counts sum to summary.files_with_findings; files without findings do not count. Only findings retained after limits contribute, so check summary.truncated for incomplete results. Triage does not change these counts, and they do not affect manifest_hash or the severity gate.

summary.files_by_severity counts distinct relative file paths at each raw severity (critical, high, medium, low, unknown), including zero counts. Repeated findings at the same severity in one file count once; a file with multiple severities counts in each applicable bucket, so the counts can sum to more than summary.files_with_findings. Counts cover only emitted findings after truncation, remain unchanged by --triage, and do not affect manifest_hash or the severity gate.

summary.secret_lines counts distinct (file, line) locations in emitted secret findings. Several hits on one line count once; the same line number in different files counts separately. A multiline private-key finding contributes only its reported header line. This is a source-location count, not a count of distinct credentials; zero means no emitted secret findings, not complete coverage. Finding limits can omit locations. The count includes low-severity hints and is unchanged by --triage; it does not affect findings, gates or manifest_hash.

summary.findings_by_path_context partitions retained findings across all categories into scaffold and other counts (both keys are present even at zero). It uses the existing case-insensitive path-segment heuristic used by packages_by_path_context, including test, fixture, example, documentation and .github directories. It counts findings, not distinct files; the two counts sum to summary.total_findings. Only findings retained after scan limits count. This path label does not establish whether a file is deployed or a finding is harmless, and it does not change raw severity, triage, or the severity gate.

summary.finding_lines_by_path_context counts distinct retained (file, line) locations in the same scaffold and other path groups, including zero counts. Findings without a line number are excluded. Multiple findings at one location count once even across categories and severities; equal line numbers in different files count separately. The two counts sum to summary.finding_lines. These are review locations after scan limits, not source coverage or proof that scaffold findings are harmless. Triage does not filter the counts.

summary.config_lines counts distinct (file, line) locations in emitted configuration findings with a line number. Multiple rules on one line count once; the same line number in different files counts separately. File-wide findings with a null line (such as a missing Dockerfile USER) are excluded. This counts only retained findings after scan limits, not every configuration line scanned.

summary.config_lines_by_rule maps configuration finding summary labels to counts of distinct (file, line) locations, sorted alphabetically by label. Repeated hits for the same rule on one line count once; the same line number in different files counts separately. Findings with line: null are excluded, and rules with no line-addressable findings are omitted (an empty report uses {}). Different rules can share a location, so adding these counts need not equal summary.config_lines. Counts describe only emitted findings after finding caps; they do not measure unchecked locations or change findings, triage, gates or hashes.

summary.config_files_by_rule maps configuration finding summary labels to distinct affected-file counts, in alphabetical label order. Repeated warnings for one rule in one file count once; the same file can count under several rules. Labels are human-readable descriptions, not stable rule IDs. Only emitted config findings count, so scan limits may omit affected files. No config findings yields {}; this does not certify complete coverage. These counts include all raw severities, are unchanged by --triage, and do not affect manifest_hash or gates.

Triage (--triage)

triage_summary.dependency_actions_by_ecosystem maps each ecosystem with retained dependency findings to upgrade and monitor finding counts (both keys are present, including zeros). Ecosystem keys are alphabetically ordered; no retained dependency findings yields {}. Missing ecosystem metadata uses unknown. Each finding counts once, regardless of advisory IDs or fixed versions; scaffold findings are included. These opt-in counts follow existing fix-metadata actions: non-empty fixed_in selects upgrade, otherwise monitor. They do not establish release availability, unique-package counts, or complete advisory coverage, and reflect finding limits. Findings, existing triage decisions, gates and manifest_hash are unchanged.

triage_summary.dependency_actions_by_file maps relative representative manifest paths to upgrade and monitor finding counts, with both buckets present for each path. Paths are sorted; the map is {} without retained dependency findings. A finding counts once regardless of advisory IDs or fixed versions: non-empty fixed_in selects upgrade, otherwise monitor. These are existing fix-metadata classifications, not confirmation of release availability. Scaffold paths are included. A package found in several manifests is attributed only to the scanner's representative path, and scan limits can truncate these counts. This opt-in summary leaves findings, triage actions, hashes and gates unchanged.

triage_summary.dependency_actions_by_severity groups emitted dependency findings into upgrade and monitor rows, each with raw-severity counts (critical, high, medium, low, unknown, including zeros). A finding counts once, regardless of its advisory IDs or fixed versions; a non-empty fixed_in selects upgrade, otherwise monitor, following the existing triage actions. Scaffold lockfiles are included at their raw severity even when triage marks their priority low. These opt-in counts cover only retained findings after scan limits and do not prove advisory coverage or that every advisory has a fix. They do not change findings, the headline, severity gates, or manifest_hash.

With --triage the output gains two additive top-level keys and the raw findings and manifest_hash stay untouched:

  • triage_summary β€” counts and a one-line headline: dependency findings split into upgradeable (nonempty fixed_in) vs monitor_only (empty fixed_in), and secret hits split into needs_review vs likely_false_positive. The headline is a human-readable summary whose wording may change between releases; consumers read counts from the structured fields (triage_summary, summary.unique_advisories), not from the headline. secret_review_by_severity breaks down the secret hits needing review by raw severity (critical, high, medium, low, unknown, including zeros). It excludes likely false positives and counts only emitted findings if the scan was truncated; these counts do not alter the raw severity gate. config.needs_review counts configuration findings and includes them in the headline, including when no secrets or dependency findings exist. Like other triage counts, it covers only emitted findings when scan limits truncate results. config_review_by_severity breaks down those configuration findings by raw severity (critical, high, medium, low, unknown, including zeros). Configuration findings under test/fixture paths still count as needing review; these counts do not alter findings, triage actions, or the raw severity gate.
  • triage β€” a map from finding id to an interpretation: for a dependency-vuln, action is upgrade (with upgrade_to) or monitor; for a secret, likely_false_positive is set true when the hit sits in a test/fixture/example/CI path, a translation-catalogue path (locales/, i18n/, …, v0.4.1), or carries a value-shape tag (placeholder, expression, non-ASCII, dotted name, form placeholder, example key β€” see the secret section), routing everything else to review; config findings are review.

Dependency actions use reported fix metadata, not an independent check of release availability. monitor means the finding has no reported fixed version: this can also happen when advisory details omit fix metadata or fail to load. It does not prove that no patched release exists; review errors and the advisories. upgrade means at least one advisory contributed a fixed version. A finding can combine multiple advisories, so a nonempty upgrade_to list does not prove that every advisory is resolved by a listed version. Confirm the applicable release against each advisory before choosing an upgrade. Existing headline and note phrases such as "no patched release" are metadata-based labels, not guarantees about available releases.

This is purely deterministic prioritisation (fix metadata + path-based false-positive classification). It never clears a secret found in real source β€” it flags it for a human. It exists so a reader does not have to hand-triage a long raw list.

triage_summary.by_action counts findings assigned to upgrade, monitor, review, and informational, including zero-valued buckets. Counts sum to the number of emitted findings, not files or advisory IDs. review includes config findings; scaffold dependency findings retain their upgrade/monitor action. When scan limits truncate the report, these counts cover only emitted findings. This field is present only with --triage and does not change the severity gate.

triage_summary.files_by_action counts distinct relative file paths for each of the same four actions, including zero-valued buckets. Repeated findings assigned to one action in a file count once; a file with different actions counts in each applicable bucket, so these counts need not sum to the total number of files. Findings without a non-empty file path do not add to these file counts. Scaffold dependencies retain their upgrade/monitor action. Counts cover only emitted findings when limits truncate the report. This field is present only with --triage and does not change findings, the hash, or the severity gate.

With --triage, triage_summary.config_files_needing_review counts distinct relative file paths with emitted configuration findings. Multiple findings in one file count once; files in test/example/fixture paths still count because configuration findings retain their review action. Secret and dependency findings do not contribute. The count is zero when no configuration files need review, and covers only retained findings when scan limits truncate results. The existing triage_summary.config.needs_review remains a finding count. This additional count does not change findings, triage actions, the hash or gate.

With --triage, triage_summary.config_review_by_file maps relative file paths to the number of retained configuration findings needing review in each file. Keys are sorted alphabetically. Multiple findings in one file count separately; test/example/fixture paths remain included. Secret and dependency findings and findings without a non-empty file path do not contribute. The mapping is empty when no configuration findings have paths. It covers only retained findings when limits truncate results, and remains available with --summary-only. This field does not change findings, triage actions, the hash or severity gate.

With --triage, triage_summary.dependency_files_needing_upgrade counts distinct representative manifest paths in emitted dependency findings with an upgrade action. Multiple packages or advisories in one file count once; scaffold paths still count. Monitor-only dependencies and non-dependency findings do not count. The count is zero when no emitted dependency finding has fix metadata. It uses the existing representative-manifest attribution after package deduplication, so it is not a count of every affected manifest. Finding limits can reduce it; this count does not establish complete advisory coverage or that an upgrade resolves every advisory. Findings, triage actions, the hash and gate are unchanged.

triage_summary.secret_files_needing_review counts distinct relative file paths with emitted secret findings classified as review. Repeated hits in one file count once; likely-false-positive secrets and non-secret findings do not count. It is zero when no secret findings need review. With scan limits reached, it counts only files represented in the emitted findings, not omitted files. Like the rest of triage, this count does not change findings, the hash, or the gate.

triage_summary.secret_review_by_file maps relative paths to the number of emitted secret findings classified as review, with paths sorted alphabetically. Repeated hits in a file count separately; informational secrets and non-secret findings are excluded. The values sum to triage_summary.secrets.needs_review, and the number of paths equals triage_summary.secret_files_needing_review. No secrets needing review produces {}. This field is present only with --triage; finding limits can make the counts partial. The classifications are heuristics, not confirmation that credentials are live. The field adds no raw values and does not change findings, the hash, or the severity gate.

With --triage, triage_summary.dependency_outside_scaffold_lockfiles contains upgradeable and monitor_only counts for emitted dependency findings whose source paths do not match the existing test/example/fixture scaffold heuristic. The existing overall dependency counts still include scaffold hits. These are finding counts, not advisory counts or recommended target versions; multiple advisories grouped into one dependency finding count once. Zero buckets are included, and finding caps can make the counts partial. Path classification helps prioritize review; it does not establish production use or reachability.

triage_summary.secret_false_positive_reasons counts emitted secret findings by their existing per-finding reason, with keys in sorted order and {} when none qualify. Each secret counts once: test paths take precedence over test filenames, localization paths, and value hints, matching per-finding triage. The counts sum to triage_summary.secrets.likely_false_positive. These are heuristic reasons, not confirmed false positives; review them before dismissing a finding. When limits truncate findings, only emitted hits count. This opt-in summary does not change raw findings, manifest_hash, or the severity gate.

triage_summary.secret_false_positives_by_file maps relative file paths to counts of emitted secret findings classified as likely false positives. Keys are sorted; the map is {} when none qualify. Multiple findings on one line count separately, and a file may contain both informational and review-needed secrets; only the informational secrets count here. Counts sum to triage_summary.secrets.likely_false_positive and include only retained findings when scan limits apply. This is a heuristic triage aid, not confirmation that a file is safe; raw findings and severity gates remain unchanged.

summary.secret_files_by_rule maps secret finding summary labels to distinct affected-file counts, in alphabetical label order. Repeated hits for one label in one file count once; the same file can count under several labels. Labels are human-readable descriptions, not stable rule IDs. Counts include all emitted secret findings, including low-severity and likely-false-positive hits, and are unchanged by --triage. Scan limits may omit affected files. No secret findings yields {}; this does not certify complete coverage. This summary does not change findings, redaction, manifest_hash, or severity gates.

summary.secret_lines_by_rule maps the same human-readable secret labels to counts of distinct (file, line) locations, in alphabetical label order. Repeated hits for one label on one line count once; the same line may count under several labels, and the same line number in separate files counts separately. All emitted secret findings count, including low-severity and likely-false-positive hits. Only retained findings count when scan limits truncate results. No secret findings yields {}; this does not certify complete coverage. This summary adds no matched values and does not change findings, manifest_hash, triage, or gates.

What it checks

1. dependency-vuln

Manifests/lockfiles parsed (files under node_modules/, vendor/, .git/, dist/, build/, target/, virtualenvs are skipped):

FileEcosystemNotes
requirements*.txtPyPIpinned == lines only; markers/comments stripped
poetry.lock, uv.lockPyPITOML [[package]] name/version
Cargo.lockcrates.ioTOML [[package]]
package-lock.jsonnpmv2/v3 packages map; falls back to v1 dependencies tree
yarn.lock (v1 + Berry)npmv1 name@range: + version "x"; Berry "name@npm:range": + version: x (workspace:/patch:/portal:/link: entries skipped, v0.4; multi-@ patch keys fixed v0.4.2)
pnpm-lock.yamlnpmpackages: keys /[email protected] or [email protected]
go.modGothe require build list (direct + // indirect); preferred over go.sum (v0.4)
go.sumGoonly when no go.mod sits beside it (go.sum also lists superseded versions); /go.mod suffix stripped
Gemfile.lockRubyGemsspecs: within GEM sections only, name (1.2.3); GIT/PATH specs excluded

Requirements files are discovered by case-sensitive basename: any name starting with requirements and ending with .txt, at any included directory depth. For example, requirements-dev.txt and nested/requirements_extra.txt qualify; Requirements.txt, requirements.in, constraints.txt, and base.txt do not. Pip include and constraint directives (-r/--requirement, -c/--constraint) are ignored, not followed. Referencing base.txt therefore does not inventory its pins. A matching requirements*.txt file is discovered independently even when no directive references it, subject to the usual directory and file limits. A directive-only requirements.txt can appear in summary.lockfiles_parsed with zero packages; that does not assess the dependencies in its include targets.

For poetry.lock, uv.lock, and Cargo.lock, the TOML reader uses package name/version pairs without inspecting source metadata. Git, local/editable, and source-less entries with a supported name/version pair can therefore enter the inventory and advisory lookup as PyPI or crates.io packages, according to the filename. Source URLs and revisions are not sent in those queries; entries with the same ecosystem, name, and version collapse into one identity even when their sources differ. A registry advisory match does not establish that a fork or local package contains the affected code. Check the source before acting on that match. --no-osv retains this inventory but skips advisory lookup.

For package-lock.json, package inventory does not filter on dev, optional, devOptional, os, or cpu metadata. Entries with these fields can enter npm advisory lookup from both the packages map and the fallback dependencies tree, including nested dependencies of development-only packages. This is a static lockfile inventory, not the packages installed by npm ci --omit=dev or on the current operating system and CPU. --no-osv keeps this inventory but skips advisory queries; a finding does not establish production installation or reachability.

For pnpm-lock.yaml, inventory comes from recognized keys in packages:, without resolving the dependency graph in importers: or snapshots:. A recognized package can enter npm advisory lookup even when no importer references it or its metadata marks it development-only, optional, or specific to another OS/CPU. Entries found only in importers or snapshots do not supply inventory. Parenthesized peer-context suffixes are removed from package keys; entries with the same name and version then count once, rather than once per peer context. --no-osv preserves this inventory but skips advisory queries. These counts do not establish which packages are installed or reachable; a parsed lockfile can still yield zero packages.

Dependency discovery matches these basenames exactly and case-sensitively at any scanned directory depth; requirements*.txt is the prefix/suffix exception. For example, Package-lock.json, package-lock.json.bak, and Requirements.txt do not supply inventory. Neither do package.json, npm-shrinkwrap.json, Pipfile.lock, pyproject.toml, or composer.lock: their dependency declarations are not parsed or sent for advisory lookup. Such files may still receive local secret/config checks. A tree containing only unrecognized manifest filenames has zero inventoried packages, an empty summary.lockfiles_parsed, and a note that no supported dependency manifest was parsed; this does not assess those dependencies.

For package-lock.json, package names in the packages map come from the path after its last node_modules/ component, including scoped names. An entry's name metadata does not override that installed name. In the legacy dependencies tree, the dependency key supplies the name and the version string is kept literally. The scanner does not resolve npm aliases: an alias can therefore be queried under its installed name rather than the underlying package name, and a legacy npm:... version is not decoded. These same identities are retained in offline inventory with --no-osv. A report without dependency findings does not establish advisory coverage for the underlying aliased package.

For package-lock.json, the legacy dependencies tree is used only when the packages map yields no package entries. An absent or empty map, a root-only map, or a map whose entries lack usable versions can therefore fall back to the legacy tree, including nested dependencies. Once the map yields even one package, the two sections are not merged: legacy-only packages and alternate legacy versions do not enter inventory or advisory queries. A partially recognized map can therefore leave legacy entries unassessed. This selection also applies to package inventory with --no-osv.

Go manifest selection is directory-local: a go.mod in the selected scan file list suppresses only the go.sum beside it, not one in a child or sibling directory. Selection happens before parsing. An empty go.mod, one with no recognized requirements, or one that cannot be read still suppresses its neighboring go.sum; the scanner does not use go.sum as a parse-failure fallback. Conversely, a go.mod absent from the selected file list does not suppress go.sum merely because it exists on disk. Review summary.lockfiles_parsed and summary.packages_found: zero packages in these cases does not mean the checksum-listed dependencies were assessed.

For package-lock.json, inventory is not filtered to production dependencies or to packages installed on the current machine. Both the v2/v3 packages map and the v1 dependencies tree include recognized name/version entries regardless of dev, optional, devOptional, os, or cpu metadata, including nested entries. These entries count in package summaries and are eligible for advisory lookup; --no-osv keeps the inventory but skips that lookup. A reported package therefore does not establish that it is installed or used in the deployed application.

For Gemfile.lock, only gems listed in GEM sections enter the package inventory and RubyGems advisory lookup. Gems listed under GIT or PATH are excluded, even if their names and versions resemble registry gems. A lockfile containing only those excluded sources can still appear in summary.lockfiles_parsed while contributing zero packages and causing no advisory requests. A clean report therefore does not assess the excluded Git or local-path dependencies.

For yarn.lock, descriptors using workspace:, patch:, portal:, link:, file:, exec:, git:, github:, http: or https: are excluded from package inventory and advisory queries, even when their entries contain numeric versions. This filter applies to each descriptor in a comma-separated list; a recognized registry descriptor in the same list can still contribute a package. Ordinary v1 ranges and Berry npm: descriptors remain supported. A lockfile containing only excluded descriptors can appear in summary.lockfiles_parsed with zero packages and no advisory requests. Such a report does not assess the excluded dependencies, including the registry packages underlying patch: descriptors.

Gemfile.lock versions are kept literally, including platform suffixes such as 1.2.3-x86_64-linux; the scanner does not normalize them to 1.2.3 for advisory lookup. The PLATFORMS section does not filter the inventoried GEM specs, so synthetic-native (1.2.3) and synthetic-native (1.2.3-x86_64-linux) count as two package/version entries and are queried separately. PLATFORMS, DEPENDENCIES and BUNDLED WITH metadata alone supply no package versions. This is lockfile inventory, not resolution of the gems installed on a particular host; an empty advisory response does not establish coverage for a platform-suffixed version.

When go.sum is used, module-content rows and /go.mod rows for the same module/version contribute one inventory entry; a /go.mod row alone also contributes an entry. Distinct versions of one module remain separate entries and advisory queries, including superseded versions. The scanner checks for a nonempty h1: checksum field but does not validate its encoding or verify its digest against module contents. This inventory is neither an installed-module list nor a checksum-integrity assessment; a passing gate establishes neither.

Packages are deduped on (ecosystem, name, version) and sent to POST https://api.osv.dev/v1/querybatch in chunks of 500. Each returned vuln id is then fetched from GET https://api.osv.dev/v1/vulns/{id} (cached in-memory per run) for severity and fixed versions.

Severity: a recognized database_specific.severity value (CRITICAL/HIGH/MODERATE/MEDIUM/LOW, case-insensitive) takes precedence over numeric scores. Otherwise the first supported numeric score in the severity list is used, mapped β‰₯9 critical, β‰₯7 high, β‰₯4 medium, else low; unsupported entries are skipped, and unknown is used when neither source is available. Within one advisory this is list order, not the highest numeric score: scores 4.0 followed by 9.8 produce medium, while reversing them produces critical. Consequently --fail-on high does not fire for the first ordering alone. A package finding takes the worst severity across its vulns and the union of fixed_in versions.

Fix metadata has additional limits: for each advisory, the scanner collects nonempty fixed event values from every affected entry and range, without filtering by the queried package, ecosystem, range type, or installed version. It ignores introduced, last_affected, and limit event values. It removes duplicates, sorts the remaining strings lexicographically (so 10.0 precedes 2.0), and retains only the first ten values per advisory. A package finding's fixed_in is the sorted, deduplicated union of those capped lists; that union can exceed ten values. With --triage, upgrade_to repeats this union. These values are advisory metadata, not a package-specific upgrade plan: they can include unrelated package versions or Git revisions, are not sorted by semantic version, and do not establish that a listed value fixes every advisory for the installed package. Check the original advisory and applicable release before choosing an upgrade.

CVSS vector strings are not evaluated. An advisory with only vector strings and no recognized database_specific.severity remains unknown. Packages with such advisories can still produce dependency findings, subject to finding limits; other rated advisories can raise a package finding's severity. unknown findings do not trigger --fail-on, even at low. A passing severity gate therefore does not clear unrated advisories.

HTTP timeout is 20 s per call. Request failures are appended to the top-level errors list and the scan continues. If a batch request fails, that chunk yields no dependency findings, while later chunks are still queried. Parsed packages remain in the inventory; an empty findings array does not establish that their advisories were checked successfully.

If an advisory-detail request fails after a batch returned its ID, the scanner keeps that ID and uses fallback details: unknown severity, no fixed versions, and the ID as the advisory summary. This fallback is cached for the rest of the scan, so a shared failed advisory is fetched and recorded as an error only once. Subject to finding limits, affected packages still produce dependency findings. Other successfully loaded advisories for the same package can supply a higher severity and fixed versions. With only failed details, --triage assigns monitor, and even --fail-on low does not fire for the resulting unknown findings. Request errors alone do not trip the severity gate; inspect errors alongside findings and inventory before interpreting a passing exit code.

2. secret

Regex scan of text files ≀ 1 MiB. Binary files (null byte), .git/, node_modules/, vendor/, dist/, build/, lockfiles, *.min.js, and common binary/image extensions are skipped. Evidence is always redacted to the first 4 characters plus ….

Secret checks operate on text, without parsing the source language or Markdown. A matching token or generic assignment inside a comment, string literal, or Markdown code block can therefore produce a finding at that source line; comment markers and code fences do not exempt it. Existing file/line limits, pattern rules, and value-shape hints still apply. --triage uses its path and value heuristics, not comment or code-block syntax, to classify these findings; the raw severity gate still includes them.

A leading dot does not by itself exclude a file or directory. For example, .settings can receive secret checks, and files under .config/ can receive secret, config and supported dependency checks. Named directory exclusions such as .git/ and .venv/, file-type exclusions, and the usual scan limits still apply; hidden paths are not a separate coverage category.

The generic-assignment check skips values starting with http://, https://, ws:// or wss:// (case-insensitive). This excludes endpoint strings, not all secrets inside URLs: provider-token patterns still report with redacted evidence. Other schemes and malformed URL prefixes do not get this exclusion.

Single-line provider patterns and generic assignments are checked only on lines of at most 4,000 characters (excluding the line ending; Unicode characters count as one character each). Longer lines are skipped, but later lines in the same file are still checked. The separate multiline private-key block check is not subject to this per-line limit; the file-size and file-selection limits above still apply. A report with no secret findings does not establish that skipped lines are free of secrets.

The 1 MiB limit is 1,048,576 file bytes, not decoded characters. A file exactly at the limit can be scanned; larger files are skipped entirely, including any secret on the first line. Other eligible files are still scanned. An oversized file can still count in summary.files_scanned; skipping its text does not set summary.truncated, whose flags describe file-count and finding-count caps. An empty findings list with both flags false therefore does not establish that all file contents were checked.

Extension exclusions apply before reading the content and are case-insensitive: even readable text in .svg / .SVG files is omitted from secret scanning. Such files still count in summary.files_scanned; skipping them does not set summary.truncated. Secrets found only in excluded files cannot trigger the severity gate, so a passing gate does not establish that those files are clear.

The secret-scan lockfile exclusion uses these exact basenames, at any depth: package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, uv.lock, Cargo.lock, Gemfile.lock, go.sum, go.mod, and composer.lock. It includes the go.mod manifest and composer.lock even though Composer dependency parsing is unsupported. It is not a blanket .lock exclusion: custom.lock and other manifests such as requirements.txt, package.json, and pyproject.toml still receive secret scanning when otherwise eligible. Supported dependency inventory/lookups run independently of this secret-scan exclusion. A parsed lockfile with no secret findings does not establish that its contents were checked for secrets.

The shared text reader tries UTF-8, then falls back to Latin-1 if UTF-8 decoding fails. ASCII token patterns can therefore still match in a Latin-1 file. Any NUL byte skips the entire file before decoding, even when a token occurs before that byte. Ordinary UTF-16/UTF-32 text containing NUL bytes is skipped rather than transcoded. Such a file can still count in summary.files_scanned; no secret findings (and a successful severity gate) do not establish that its contents were assessed.

The *.min.js exclusion is a case-sensitive filename suffix check, not content analysis: even formatted text named bundle.min.js is skipped. Names such as bundle.MIN.JS and bundle.min.js.map, and compact code in ordinary .js files, are not excluded by this rule; other scan limits still apply. Skipped .min.js files can count toward summary.files_scanned without setting summary.truncated. Secrets present only in those files cannot trigger the severity gate, so a passing gate does not establish that they were checked.

PatternSeverity
AKIA[0-9A-Z]{16} (AWS access key id)high
gh[pousr]_[A-Za-z0-9]{36,}high
github_pat_[A-Za-z0-9_]{80,}high
glpat- + 20 URL-safe characters (GitLab personal access token)high
npm_ + 36 alphanumerics (npm access token)high
sk- or sk-proj- + 40+ URL-safe characters (OpenAI; excludes sk-ant-)high
sk-ant- + 40+ URL-safe characters (Anthropic)high
hf_ + 34+ alphanumerics (Hugging Face)medium
SG. + 22 URL-safe characters + . + 43 URL-safe characters (SendGrid)high
digits + : + 35 URL-safe characters (Telegram bot token)high
SK + 32 hex characters (Twilio API key SID)medium
xox[baprs]-[0-9A-Za-z-]{10,} (Slack)high
sk_live_[0-9a-zA-Z]{24,} (Stripe live)critical
AIza[0-9A-Za-z_-]{35} (Google API key)medium
Complete multiline BEGIN … PRIVATE KEY / matching END blockcritical
Known private-key header followed by a base64-looking line (legacy fallback)critical
generic api_key / api-key / apikey / secret / password / passwd / token assignmentmedium
Stripe live restricted key (rk_live_ + 24+ alphanumeric characters)high

Generic assignment names above are case-insensitive; bare key is not recognized by this rule. Assignments use = or : followed by a single- or double-quoted value of at least 16 characters with no whitespace or embedded quotes. Generic assignment keys may also be enclosed in paired single or double quotes, such as JSON's "password": "…16+ chars". This is a line-based heuristic, not a JSON/YAML parser; the existing value exclusions and hint downgrades still apply.

URL-safe characters above are A-Z, a-z, 0-9, _, and -.

Complete private-key blocks accept any uppercase/space label (including unlabelled PRIVATE KEY), require at least three base64 body lines, and produce one Multiline private key block detected finding on the header line. That finding replaces the legacy header hit. Body hints reuse the existing example/mask and value-shape checks to downgrade to low; a short placeholder body such as ..., YOUR_KEY_HERE, REDACTED, or xxxx is also retained as a low finding. Evidence never includes the body. A bare header in source without a body remains ignored.

The legacy header fallback also emits a critical Private key block detected finding for an unlabelled, RSA, EC, OPENSSH, DSA or PGP private-key header when the next line contains at least 20 characters from A-Z, a-z, 0-9, +, / or = (with optional surrounding whitespace). This fallback does not require a footer or a complete key, and does not apply body placeholder downgrades. Its evidence contains only the redacted header. These are text-shape checks, not proof that the body is valid base64 or usable private-key material.

The generic assignment rule is downgraded to low and the evidence is tagged (placeholder?) when the value matches example|changeme|your[_-]|xxx|dummy|placeholder|<|${.

Value-shape hints (v0.4.1). The 2026-10-08 dataset showed that most secret hits surviving the path rule were not credentials at all, so the evidence now carries one of these tags and the raw severity drops to low when the value matches a shape heuristic; --triage classifies the same tags as likely false positives:

tagshape
(expression?)template / env reference: ={{$credentials.x}}, ${VAR}, $VAR, $__env{…}, {env:…}, #{…}, %{…}, <%…, var(--…)
(non-ascii?)generic assignment value contains no
How do I install Feldspar Free Repository Security Scan?
Copy the http config for Cursor, Claude Desktop, Claude Code, or VS Code on this page. It uses the publisher's install, not a PlugMCP proxy.
Does Feldspar Free Repository Security Scan need an API key?
No required environment variable is listed on this record.
What transport does Feldspar Free Repository Security Scan use?
Feldspar Free Repository Security Scan is reached over HTTP.
When was Feldspar Free Repository Security Scan last checked?
Last checked 2026-10-16. Free deterministic security scan of public git repos: OSV.dev vulnerable deps, secrets, config lint.

No tool list yet. Remote servers get one after the first health check.

plugmcp

Plug any tool into any agent. MCP servers for people who do not write servers. Gateway for volume, n8n for apps.

  • github.com/TODO
  • x.com/TODO
  • linkedin.com/company/TODO

PlugMCP, 2026

Product
  • Browse
  • Install
  • Host your own
  • Voice mode
  • Browser
Directory
  • All MCP servers
  • Agent skills
  • Categories
  • New
  • Voice
  • Submit a server
Developers
  • Docs
  • Blog
  • What is MCP
  • What is WebMCP
  • Status
Company
  • About
  • Contact
  • Terms
  • Privacy
  • DPA