Advanced

Trust Scoring

How SCAL-P computes trust scores — hash verification, maturity, downloads, and CVEs. Offline-first, deterministic, no magic.

A package with 1M downloads/week and no known CVEs should not be treated the same as a random 0.0.1 from an unknown author. Trust score gives you a numeric dimension on top of allow/deny.

Quick reference

FactorMaxThresholds
Hash verified30Has lockfile entry = 30, missing = 0
Version maturity15major >= 1 = 15, major < 1 = 0
Weekly downloads20100K+ = 20, 10K+ = 15, 1K+ = 10, 100+ = 5, under 100 = 0
No active CVEs15Audit clean = 15, has CVEs = 0, unknown = 7
Total80Offline-safe: network failure = half points

What it is

A deterministic score (0–80) for each dependency, computed from four factors:

FactorMax ptsSourceWorks offline?
Hash verified30.scalp/lockfile.jsonyes
Version >= 1.0.015lockfileyes
Weekly downloads20api.npmjs.orgdegraded (10 pts)
No active CVEs15npm audit --jsondegraded (7 pts)

If the score is below trust.min_score in your policy, it's a violation — same as a denied package.

Unknown vs Bad

This is the key distinction that makes the score fair when offline.

Unknown = we can't check right now (no internet, no audit data, no cache). You get half points as a penalty, not zero. No one likes uncertainty, but we don't assume the worst.

Bad = we checked and the data says it's bad. You get 0.

FactorUnknown (offline/no data)Bad (checked & failed)
Downloads10 pts0 pts (< 100/week)
CVEs7 pts0 pts (open CVEs found)

Download example:

  • Offline, no cache → 10 pts (unknown)
  • Online, 50 downloads/week → 0 pts (bad — low popularity)
  • Online, 500K downloads/week → 20 pts (good)

CVE example:

  • Pre-install (no node_modules to audit) → 7 pts (unknown)
  • npm audit ran, found CVEs for this package → 0 pts
  • npm audit ran, no CVEs → 15 pts

Hard fail: require_hash

{ "trust": { "require_hash": true } }

When require_hash is true, any package that lacks a lockfile integrity entry is an automatic violation — regardless of total score. The violation says hash_required and the package is skipped from trust scoring entirely.

This is your "supply chain minimum" switch. If a package wasn't installed through SCAL-P's guarded flow (or was tampered with after), you know immediately — not just see a lower score.

Both require_hash and min_score can be active at the same time:

Package stateResult
Missing hashViolation (hash_required)
Has hash, score 30/50Violation (trust_score_too_low)
Has hash, score 65/50Passes

The four factors in detail

Hash verified (30 pts)

Your lockfile stores SHA-512 hashes of installed packages. If a package has a non-empty integrity entry, that's 30 points. No entry = 0 (or hash_required violation if enabled).

This rewards packages that were installed through SCAL-P's guarded flow. Manual installs or lockfile edits get 0.

Version maturity (15 pts)

major >= 1 → 15. Anything below 1.0.0 is pre-release.

Parsing: split on ".", parse first component. ^0.5.0, ~1.2.3, v2.0 all work.

VersionPoints
1.0.015
2.3.415
0.5.00
0.0.10

Weekly downloads (0–20 pts)

Thresholds are logarithmic:

Downloads/weekPoints
< 1000
100–9995
1,000–9,99910
10,000–99,99915
100,000+20

Fetched from GET https://api.npmjs.org/downloads/point/last-week/{name}. Cached in .scalp/cache/trust.json for 7 days.

Network failure with no cache → 10 pts (unknown). Network failure with stale cache → uses stale cache.

HTTP call has a 10s timeout. If it fails, the scorer moves on — no blocking.

No active CVEs (0 or 15 pts)

Runs npm audit --json once per evaluation, maps vulnerabilities by package and version.

npm audit succeeded:

  • Package has open CVEs → 0 pts
  • Package has no CVEs → 15 pts

npm audit failed (pre-install, no lockfile, etc.):

  • Cache has a CVE entry for this specific version → 0 pts
  • Cache has a clean entry for this version → 15 pts
  • No cache for this version → 7 pts (unknown)

Cache

File: .scalp/cache/trust.json — auto-managed, never commit.

{
  "lodash": {
    "fetched_at": "2026-05-13T12:00:00Z",
    "weekly_downloads": 142536,
    "versions": {
      "4.17.21": {
        "fetched_at": "2026-05-13T12:00:00Z",
        "cves": []
      },
      "4.17.20": {
        "fetched_at": "2026-05-10T12:00:00Z",
        "cves": ["GHSA-xxx"]
      }
    }
  }
}
  • Top-level keys: package names
  • weekly_downloads: per-package (same for all versions)
  • versions: maps exact version strings to per-version data (CVEs)
  • TTL: 7 days from fetched_at

The scorer loads the cache once, reads/writes entries during scoring, and saves at the end — only if something changed (dirty flag).

Violation messages

Trust violations include a breakdown so you know why:

trust_score: 17/50 (hash:0, maturity:0, dl:10, cves:7)

This tells you: no hash, no maturity, unknown downloads (10/20), unknown CVEs (7/15). One glance and you know the package is new and offline.

hash_required: package integrity not in lockfile

This tells you: require_hash is on and this package isn't tracked.

Enforcement

Trust violations follow the same enforcement as policy violations:

  • "block" → exits 1
  • "warn" → logs and continues
  • "log" → silent pass

There's no separate enforcement mode for trust. If you want trust to block but allowlist to warn, you can't — yet.

What it does NOT do

  • No 2FA / verified email (npm doesn't expose this per-package)
  • No Sigstore / provenance
  • No typosquatting detection
  • No persistent network daemon — every CLI call is stateless

On this page