Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
109b2f7
01/70 npm-health: add helper module foundation
Berserk-hub150 Aug 17, 2026
e4daf6a
02/70 npm-health: define pipeline errors
Berserk-hub150 Aug 17, 2026
d676633
03/70 npm-health: parse package URLs
Berserk-hub150 Aug 17, 2026
f0c395a
04/70 npm-health: validate versioned npm PURLs
Berserk-hub150 Aug 17, 2026
c77dfe3
05/70 npm-health: support scoped npm package names
Berserk-hub150 Aug 17, 2026
32da568
06/70 npm-health: build registry metadata URLs
Berserk-hub150 Aug 17, 2026
a65c89f
07/70 npm-health: normalize repository URLs
Berserk-hub150 Aug 17, 2026
78ad4ca
08/70 npm-health: extract repository metadata
Berserk-hub150 Aug 17, 2026
5bce0c2
09/70 npm-health: extract package tarball URLs
Berserk-hub150 Aug 17, 2026
a4817e7
10/70 npm-health: extract package homepage URLs
Berserk-hub150 Aug 17, 2026
8f91b47
11/70 npm-health: extract package license metadata
Berserk-hub150 Aug 17, 2026
4ed3060
12/70 npm-health: count npm maintainers
Berserk-hub150 Aug 17, 2026
a64fb1a
13/70 npm-health: count runtime dependencies
Berserk-hub150 Aug 17, 2026
a8f95d9
14/70 npm-health: fetch exact npm registry metadata
Berserk-hub150 Aug 17, 2026
4262ae1
15/70 npm-health: clamp normalized values
Berserk-hub150 Aug 17, 2026
8b43778
16/70 npm-health: normalize metric values
Berserk-hub150 Aug 17, 2026
ecf9ca5
17/70 npm-health: normalize collector payloads
Berserk-hub150 Aug 17, 2026
32a9a80
18/70 npm-health: derive baseline registry metrics
Berserk-hub150 Aug 17, 2026
c959a20
19/70 npm-health: merge external collector metrics
Berserk-hub150 Aug 17, 2026
0d507f9
20/70 npm-health: normalize scoring weights
Berserk-hub150 Aug 17, 2026
a03c5e4
21/70 npm-health: compute weighted health scores
Berserk-hub150 Aug 17, 2026
7ceab4f
22/70 npm-health: classify package health scores
Berserk-hub150 Aug 17, 2026
d12c0b1
23/70 npm-health: parse cached collection timestamps
Berserk-hub150 Aug 17, 2026
acb1669
24/70 npm-health: detect stale cached analyses
Berserk-hub150 Aug 17, 2026
0df7941
25/70 npm-health: expose collection targets
Berserk-hub150 Aug 17, 2026
140d9fe
26/70 npm-health: build external collector context
Berserk-hub150 Aug 17, 2026
bb82957
27/70 npm-health: render shell-free collector commands
Berserk-hub150 Aug 17, 2026
7987797
28/70 npm-health: execute external metrics collectors
Berserk-hub150 Aug 17, 2026
32cf473
29/70 npm-health: load external metrics JSON
Berserk-hub150 Aug 17, 2026
c0e0749
30/70 npm-health: integrate external metrics adapter
Berserk-hub150 Aug 17, 2026
429e4af
31/70 npm-health: build reusable result snapshots
Berserk-hub150 Aug 17, 2026
e1469da
32/70 npm-health: read cached project snapshots
Berserk-hub150 Aug 17, 2026
70a8e8d
33/70 npm-health: persist snapshots in project extra data
Berserk-hub150 Aug 17, 2026
192a859
34/70 npm-health: write portable JSON result output
Berserk-hub150 Aug 17, 2026
82ae20f
35/70 npm-health pipeline: add pipeline class
Berserk-hub150 Aug 17, 2026
93bb0ba
36/70 npm-health pipeline: validate project PURL
Berserk-hub150 Aug 17, 2026
0b6cba6
37/70 npm-health pipeline: reuse fresh cached results
Berserk-hub150 Aug 17, 2026
4a57e17
38/70 npm-health pipeline: fetch registry metadata
Berserk-hub150 Aug 17, 2026
f3c4652
39/70 npm-health pipeline: collect package metrics
Berserk-hub150 Aug 17, 2026
7ef6c25
40/70 npm-health pipeline: compute package score
Berserk-hub150 Aug 17, 2026
c98605a
41/70 npm-health pipeline: build result snapshot
Berserk-hub150 Aug 17, 2026
7e7afb3
42/70 npm-health pipeline: persist package results
Berserk-hub150 Aug 17, 2026
dad03c9
43/70 npm-health pipeline: define execution steps
Berserk-hub150 Aug 17, 2026
65d0754
44/70 npm-health: register built-in pipeline entry point
Berserk-hub150 Aug 17, 2026
4ddcaf0
45/70 npm-health tests: cover valid npm PURLs
Berserk-hub150 Aug 17, 2026
c53cce3
46/70 npm-health tests: reject missing project PURLs
Berserk-hub150 Aug 17, 2026
e86fdb3
47/70 npm-health tests: reject non npm PURLs
Berserk-hub150 Aug 17, 2026
256e0e3
48/70 npm-health tests: require package versions
Berserk-hub150 Aug 17, 2026
db1357c
49/70 npm-health tests: support scoped package names
Berserk-hub150 Aug 17, 2026
fff432b
50/70 npm-health tests: build scoped registry URLs
Berserk-hub150 Aug 17, 2026
1f78d3d
51/70 npm-health tests: normalize repository dictionaries
Berserk-hub150 Aug 17, 2026
e91a720
52/70 npm-health tests: normalize GitHub git transports
Berserk-hub150 Aug 17, 2026
35be255
53/70 npm-health tests: expose registry collection targets
Berserk-hub150 Aug 17, 2026
806184f
54/70 npm-health tests: normalize fractional metrics
Berserk-hub150 Aug 17, 2026
a6a552f
55/70 npm-health tests: normalize percentage metrics
Berserk-hub150 Aug 17, 2026
c36f9f9
56/70 npm-health tests: normalize nested collector payloads
Berserk-hub150 Aug 17, 2026
73a57d1
57/70 npm-health tests: derive registry metrics
Berserk-hub150 Aug 17, 2026
b117073
58/70 npm-health tests: merge external metrics
Berserk-hub150 Aug 17, 2026
e5b6eaa
59/70 npm-health tests: normalize positive scoring weights
Berserk-hub150 Aug 17, 2026
b67c2c4
60/70 npm-health tests: compute weighted scores
Berserk-hub150 Aug 17, 2026
2303e06
61/70 npm-health tests: classify health scores
Berserk-hub150 Aug 17, 2026
2efc1f3
62/70 npm-health tests: keep fresh cached analyses
Berserk-hub150 Aug 17, 2026
0fc3656
63/70 npm-health tests: expire stale cached analyses
Berserk-hub150 Aug 17, 2026
724cc74
64/70 npm-health tests: render collector commands without a shell
Berserk-hub150 Aug 17, 2026
e65bebb
65/70 npm-health tests: reject invalid collector JSON
Berserk-hub150 Aug 17, 2026
89eb9f0
66/70 npm-health tests: read cached snapshots
Berserk-hub150 Aug 17, 2026
8bdbe92
67/70 npm-health tests: persist project snapshots
Berserk-hub150 Aug 17, 2026
5aa038d
68/70 npm-health tests: cover pipeline execution contract
Berserk-hub150 Aug 17, 2026
1de3b45
69/70 docs: add npm-health built-in pipeline reference
Berserk-hub150 Aug 17, 2026
3add896
70/70 docs: document npm-health adapter and output
Berserk-hub150 Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/built-in-pipelines.rst
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,36 @@ Fetch Scores (addon)
.. autoclass:: scanpipe.pipelines.fetch_scores.FetchScores()
:members:
:member-order: bysource


.. _pipeline_npm_health:

NPM Health (addon)
------------------

The ``npm_health`` pipeline analyzes one exact versioned npm ``Project.purl``.
It retrieves npm registry metadata, records repository and package tarball
locations, normalizes health signals, computes a weighted score from 0 to 100,
and stores a reusable snapshot in ``Project.extra_data["npm_health"]``.

.. autoclass:: scanpipe.pipelines.npm_health.NpmHealth()
:members:
:member-order: bysource


NPM Health configuration
~~~~~~~~~~~~~~~~~~~~~~~~

Fresh snapshots are reused for 90 days by default. Override the age with
``npm_health_cache_max_age_days`` in project pipeline settings.

Optional GrimoireLab or other project-health metrics can be connected through
``npm_health_metrics_command``. The command is parsed to an argument list and
executed without a shell. It may use ``{purl}``, ``{repository_url}``,
``{tarball_url}``, and ``{output}`` placeholders. The collector must write JSON
metrics to ``{output}``, either directly or under a top-level ``metrics`` key.
Metric values may be fractions from 0 to 1 or percentages from 0 to 100.

The optional ``npm_health_metric_weights`` mapping customizes scoring weights.
Only metrics present in the current analysis participate in the denominator.
The pipeline also writes a timestamped ``npm-health-*.json`` project output.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ collect_symbols_pygments = "scanpipe.pipelines.collect_symbols_pygments:CollectS
collect_symbols_tree_sitter = "scanpipe.pipelines.collect_symbols_tree_sitter:CollectSymbolsTreeSitter"
enrich_with_purldb = "scanpipe.pipelines.enrich_with_purldb:EnrichWithPurlDB"
fetch_scores = "scanpipe.pipelines.fetch_scores:FetchScores"
npm_health = "scanpipe.pipelines.npm_health:NpmHealth"
find_vulnerabilities = "scanpipe.pipelines.find_vulnerabilities:FindVulnerabilities"
inspect_elf_binaries = "scanpipe.pipelines.inspect_elf_binaries:InspectELFBinaries"
inspect_packages = "scanpipe.pipelines.inspect_packages:InspectPackages"
Expand Down
124 changes: 124 additions & 0 deletions scanpipe/pipelines/npm_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# SPDX-License-Identifier: Apache-2.0
#
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
# The ScanCode.io software is licensed under the Apache License version 2.0.
# Data generated with ScanCode.io is provided as-is without warranties.
# ScanCode is a trademark of nexB Inc.
#
# You may not use this software except in compliance with the License.
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

"""Collect and score npm package health information."""

from scanpipe.pipelines import Pipeline
from scanpipe.pipes import npm_health


class NpmHealth(Pipeline):
"""Collect reusable npm package health metrics for one project PURL."""

download_inputs = False
is_addon = True
results_url = "/project/{slug}/"


def validate_project_purl(self):
"""Validate and parse the project's versioned npm PURL."""
self.package = npm_health.validate_npm_package_url(self.project.purl)


def load_cached_snapshot(self):
"""Reuse a fresh project snapshot when one is already available."""
self.cached_snapshot = None
snapshot = npm_health.get_cached_snapshot(self.project)
max_age = int(
self.env.get(
"npm_health_cache_max_age_days",
npm_health.DEFAULT_CACHE_MAX_AGE_DAYS,
)
)
if snapshot and not npm_health.is_stale(snapshot, max_age_days=max_age):
self.cached_snapshot = snapshot
self.snapshot = snapshot
self.log("Using fresh cached npm-health analysis.")


def fetch_package_metadata(self):
"""Fetch exact npm registry metadata unless a fresh cache is used."""
if self.cached_snapshot:
return
self.metadata = npm_health.fetch_registry_metadata(self.package)


def collect_package_metrics(self):
"""Collect built-in registry signals and optional external metrics."""
if self.cached_snapshot:
return

baseline = npm_health.collect_registry_metrics(self.metadata)
external = {}
command_template = self.env.get("npm_health_metrics_command")
if command_template:
output = self.project.tmp_path / "npm-health-external-metrics.json"
external = npm_health.collect_external_metrics(
command_template=command_template,
purl=self.project.purl,
metadata=self.metadata,
output=output,
cwd=self.project.tmp_path,
)
self.metrics = npm_health.merge_metrics(baseline, external)


def compute_package_health_score(self):
"""Compute the normalized package health score."""
if self.cached_snapshot:
return
weights = self.env.get("npm_health_metric_weights")
self.score = npm_health.compute_health_score(self.metrics, weights=weights)


def build_result_snapshot(self):
"""Build the reusable snapshot stored on the project."""
if self.cached_snapshot:
return
self.snapshot = npm_health.build_snapshot(
purl=self.project.purl,
metadata=self.metadata,
metrics=self.metrics,
score=self.score,
)


def persist_results(self):
"""Persist fresh results to extra_data and a portable JSON output."""
if self.cached_snapshot:
return

npm_health.cache_snapshot(self.project, self.snapshot)
output = npm_health.write_snapshot(self.project, self.snapshot)
self.project.add_info(
model="npm_health",
description=(
f"npm-health score: {self.snapshot['score']} "
f"({self.snapshot['classification']})"
),
details={"output": output.name},
)


@classmethod
def steps(cls):
return (
cls.validate_project_purl,
cls.load_cached_snapshot,
cls.fetch_package_metadata,
cls.collect_package_metrics,
cls.compute_package_health_score,
cls.build_result_snapshot,
cls.persist_results,
)
Loading