Skip to content
nerlo.ai

Methodology

Every score on this site is produced by a deterministic formula: the same findings always produce the same score. This page documents that formula exactly, every constant, threshold, and deduction, so you can recompute any number you see here and check our work. If a score looks wrong, the formula (or a scanner) is wrong, and we want to know.

Two audiences, two places to start: how scoring works if you are deciding whether to install a package, or for publishers if you ship one and want to know why it scored the way it did.

No findings does not mean no risk. Nerlo aggregates automated scanner output; automated scanning is one layer of defence, not a substitute for review.

An aggregator, not an oracle

We run a suite of scanners over each package and show you every verdict separately. We are a transparency aggregator, not a trust authority: no scanner is privileged, none can veto another, and the composite is a subordinate summary layered on top, not the headline. Each scanner produces its own scorecard with its own score, findings, and tool version, and those per-scanner reports are the primary view. The composite trust score is a convenience for readers who want one number; it never hides, overrides, or reranks a per-scanner verdict.

Eight of the scanners in the suite are off-the-shelf tools that we pin and run as-is:

Three are our own. We say so plainly here and label them Nerlo-original everywhere the suite is listed, because you should never have to guess which verdicts came from a third party and which came from us:

Provenance is a label, not a rank. A finding from a scanner we wrote deducts exactly what the same finding from an off-the-shelf tool would, and neither can overrule the other. Because each scanner is shown on equal footing, a clean report from one tool never launders a critical finding from another. When you disagree with a scanner, deselect it and the composite recomputes from only the tools you trust, described under “Your view” below.

Every distribution surface, scanned

A single MCP server rarely ships from a single place. The same package can reach you as git source, a GitHub release asset, an npm tarball, or a pip wheel, and those copies do not have to be identical. We acquire every distribution surface we can find for a package and scan each one independently, rather than trusting that the code on the repository's main branch is the code that actually gets installed.

Acquisition happens inside a sandbox that never executes untrusted package code. Nothing is installed, built, or run while a surface is fetched. Each package is reduced to a tree of files that the static scanners then read without ever invoking it.

Nothing is executed, including by us

Every phase that runs on this registry reads files. We never execute the package we are scanning, at any point in a scan. The scan container is started with sleep as its own command, the scanner binaries are run inside it against a copy of the package mounted read-only, and the package's own entry point is never invoked. The single place we run a package manager at all is lockfile resolution, which uses npm install --package-lock-only --ignore-scripts precisely so that no install hook fires.

We state that as a limit rather than a feature, because the design called for more. The pipeline has four phases. The two that run are static: static analysis, and the dependency and supply-chain audit. The two that would have executed the package are held back, and you should hear which from us rather than infer it from a scorecard.

Both stay reserved until containment can be measured rather than assumed, and you do not have to take that on faith. The weights actually applied to a scan are published with its results, and the fuzzing and deep-behavioral terms read 0.0 on every scan in this registry. If that day never comes, they stay at zero and this page keeps saying so.

Surface divergence: the source-vs-published diff

Once we hold more than one surface for the same package, we diff them against each other. The signal we care about most is behavior that is present in a published or release package but absent from the source it claims to be built from: an npm tarball or a release asset that contains network calls, credential reads, or obfuscated logic that never appears in the git tree.

This is exactly the shape of the postmark-mcp and FakeGit supply-chain-divergence incidents, where the repository looked clean but the shipped package did not. A methodology that only scans the source repository is blind to that class of attack by construction. Scanning every surface and flagging the divergence is how we catch it: the divergence itself is a finding, surfaced in the report rather than averaged away.

How scoring works

Scoring runs in two layers. Each scanner scores the package it just read, and then those scores combine into one composite. Both layers use the same badge ladder, and both are arithmetic over findings. No model sits in between. The constants are ours and so are the weights; we publish the reasoning for each one below so you can argue with the choice rather than take the number on faith.

One badge ladder

Per-scanner badges and the composite badge use the same thresholds, so there is a single ladder to learn. It is not the whole rule for the composite badge, though: two floors sit on top of it, described immediately below the table.

BadgeScore rangeAPI value
Clean85 – 100Verified
Caution60 – 84.99Caution
Flaggedbelow 60Unsafe

The third column is the value the API returns and the value ?badge= takes, which is unchanged. We renamed only what you read. “Unsafe” was a claim the evidence did not support: it means “at least one scanner scored this below 60”, which on our own corpus was true of packages scoring 95 out of 100 with no critical finding anywhere. “Verified” failed in the other direction, because we do not verify anything — we run tools and publish what they found. “Caution” is unchanged; it was already advice to a reader rather than a verdict on someone’s work.

The composite badge is not the composite score

This is the part of our methodology most likely to surprise you, so it gets its own section rather than a footnote. If you take the composite score off a package page and read it against the ladder above, you will sometimes get a greener badge than the one we publish. That is not a bug and it is not a stale number. Two rules floor the composite badge after the score is computed, and both can only ever make the badge worse than its score alone would suggest. Neither ever makes it better.

Why we do this. The composite is an average, and averages dilute. Without the floor, a majority of clean scanners could arithmetically outvote the one scanner that found something serious, and the badge would read Clean while a scorecard one scroll down said otherwise. We would rather the summary be too pessimistic than quietly launder a finding. The cost of that choice is the surprise this section exists to remove.

A worked example, from a real shape in our registry. A package is scanned. Every scanner that applied but one returns Clean, most of them at 100. The one dissenter returns 58, two points under the Caution cutoff, on a pair of high findings and nothing worse. No scanner anywhere reports a critical. The composite score, a weighted mean, comes out at 96.33, which sits well inside the Clean band. The published badge is Flagged, because the worst completed scanner said Flagged and the floor carries that verdict up. Both numbers are correct and we publish both. The score tells you what the suite concluded on average; the badge tells you the worst thing any one tool found.

The consequence worth stating plainly: a package badged Flagged has not necessarily scored below 60. Most of the ones in our registry have not. Read the badge as “at least one scanner raised something,” then open the per-scanner grid to see which scanner, what it found, and whether you agree with it. That grid is the primary view for exactly this reason.

What a badge does not tell you

A badge summarises what a fixed set of automated tools reported about one version of one package at one moment. It is not a certification, an endorsement, or a judgement about the people who wrote the code. Being specific about the edges:

No findings does not mean no risk. Nerlo aggregates automated scanner output; automated scanning is one layer of defence, not a substitute for review.

If you publish one of these packages and think a badge misrepresents it, here is what you can do about it.

Per-scanner scores

A scanner's score starts at 100 and loses points for each finding, by severity. The result is clamped to 0–100.

Finding severityDeductionNote
Critical40also caps the score at 59 (Flagged) even if the arithmetic lands higher
High15also caps the score at 84 (Caution): a high finding can never hold Clean
Medium5one medium alone stays Clean (95)
Low2
Informational0.5

The composite score

The composite is a weighted mean over the components we could actually assess. Each scan phase's component score is the average of the scanners that completed in that phase; the weights below are renormalized to sum to 1.0 across only the phases that produced a result. A phase that has not been built yet, or that no scanner found applicable, is left out of the average entirely. It is never counted as a zero, so a clean partial scan is not dragged down by work we haven't done.

The weights are choices, not measurements. Each row says why we picked it, and the exact weights applied to a given scan are published with its results. The fuzzing, deep-behavioral, and vitality weights have never been applied to anything. The first two are the dynamic phases described under nothing is executed; the third needs inputs nothing collects yet. They are marked below, and marked rather than deleted, because the constants are real and the renormalization above is what removes them. What that leaves is narrow and we would rather you heard it here: every composite in this registry is drawn from static analysis and dependencies alone — 0.5 each where both phases completed, and static analysis at 1.0 where the dependency phase produced nothing. Where neither completed there is no composite to publish.

ComponentDefault weight
Static analysis (SAST)Reads every file we acquired, so it is the broadest signal we have. It gets a full quarter.0.25
Dependencies / supply chainMost compromises arrive through something the package pulls in rather than through code its publisher wrote. We weight it equal to the code itself.0.25
Fuzzing / injection resistanceNever appliedReserved. This is the phase that would send adversarial input to a running server, and we do not run servers, so it has never contributed to a score on this registry.0.250.0 applied
Deep behavioral auditNever appliedReserved. This is the phase that would execute the package and watch it. We could not prove the containment it needs, so we did not ship it, and it has never contributed to a score on this registry.0.150.0 applied
Maintenance vitalityNever appliedReserved. The sub-scores below are implemented and tested, but nothing in the scan pipeline collects the commit, issue, and dependency-age inputs they need, so no scan has ever supplied a vitality value.0.100.0 applied

A worked example

Every number below comes from the two tables above. Nothing else is needed to reproduce it, and you can run the same arithmetic against any scorecard we publish.

Step 1: one scanner. A static-analysis scanner reports one high, two medium, and three low findings. Starting from 100: −15 for the high, −5 twice for the mediums, −2 three times for the lows. That is 100 − 15 − 10 − 6 = 69. The high finding also caps the score at 84, which does not bite here because 69 is already below it. 69 sits in Caution.

Step 2: the component scores. Say that scanner was the only one to complete in its phase, so the static-analysis component is 69. Dependencies came out at 90. Those are the two phases that run, and they are the two components this scan produced. Fuzzing, deep behavioral, and vitality produced nothing, so all three are dropped from the average rather than scored zero.

Step 3: renormalize, then combine. The two surviving default weights (0.25 and 0.25) sum to 0.50, so each is divided by 0.50 to bring the total back to 1.0:

ComponentScoreWeightContribution
Static analysis (SAST)690.500034.50
Dependencies900.500045.00
Composite79.50

79.50 reads as Caution, and the badge floor agrees: the one scanner that completed came out at 69, which is itself Caution, so the composite badge could not have read Clean regardless of the arithmetic. This is the shape of a real scan here whenever both phases complete: two components, renormalized over 0.50, and no behavioral deduction, because there is no phase that could produce one.

Maintenance vitality

This component is specified and implemented, and it has never run. Nothing in the scan pipeline collects the commit history, issue response times, or dependency ages it needs, so no scan has supplied a vitality value and the term is renormalized away exactly like the two dynamic phases. We document it because the arithmetic is real and the field is in the API response you can read; we are not claiming it contributed to any score you have seen. When it does run, it will work as follows.

The vitality component measures whether a project is actively maintained, over the trailing 90 days. It is the mean of three sub-scores, each linearly interpolated and clamped to 0–100:

Where a signal simply doesn't exist (no issues ever filed, no dependencies), that sub-score gets full marks: absence of data is not treated as evidence of neglect.

“Your view”: the scanner filter

When you deselect scanners on a package page, the composite is recomputed from only the scanners you kept, each with equal weight (vitality joins as one additional equally-weighted term when there is a vitality value, which today there never is). Phase weights are deliberately dropped: the filter expresses “these are the tools I trust,” not “these are the phases I care about.” The behavioral-deduction term described above is not filterable either, but that is a property of the formula rather than something you will observe: it has never been non-zero, because nothing executes a package and therefore nothing can trip a decoy.

For publishers

If you ship one of these packages and you think the score is wrong, the remedy is the scan itself, not a conversation with us.

Check our work

Every scan stores its per-scanner reports, component scores, and the exact weights applied, immutably for 24 months. The scoring engine is a pure function: same inputs, same score. If you recompute a number from this page and get a different answer than we show, see our Trust page for how to verify the scan pipeline itself, and tell us, publicly.