diff --git a/backend/src/api/public/v1/packages/getPackage.ts b/backend/src/api/public/v1/packages/getPackage.ts index 8c4414e170..69806af604 100644 --- a/backend/src/api/public/v1/packages/getPackage.ts +++ b/backend/src/api/public/v1/packages/getPackage.ts @@ -40,6 +40,7 @@ export async function getPackage(req: Request, res: Response): Promise { const scorecardScore = pkg.scorecardScore != null ? Number(pkg.scorecardScore) : null const mappingConfidence = pkg.repoMappingConfidence != null ? Number(pkg.repoMappingConfidence) : null + const healthBandScore = pkg.healthScore !== null ? pkg.healthScore / 10 : scorecardScore ok(res, { purl: pkg.purl, @@ -47,8 +48,15 @@ export async function getPackage(req: Request, res: Response): Promise { ecosystem: pkg.ecosystem, latestVersion: pkg.latestVersion ?? null, general: { - healthScore: scorecardScore !== null ? Math.round(scorecardScore * 10) : null, - healthBand: computeHealthBand(scorecardScore), + healthScore: pkg.healthScore, + healthScoreDetails: { + total: pkg.healthScore, + label: pkg.healthLabel, + maintainerHealth: pkg.maintainerHealthScore, + securitySupplyChain: pkg.securitySupplyChainScore, + developmentActivity: pkg.developmentActivityScore, + }, + healthBand: computeHealthBand(healthBandScore), impact: { impactScore: pkg.criticalityScore != null ? Math.round(Number(pkg.criticalityScore) * 100) : null, @@ -58,7 +66,7 @@ export async function getPackage(req: Request, res: Response): Promise { transitiveReach: pkg.transitiveReach, }, riskSignals: { - lifecycle: null, + lifecycle: pkg.lifecycleLabel, maintainerBusFactor: pkg.maintainerCount, lastRelease: pkg.latestReleaseAt ? pkg.latestReleaseAt.toISOString() : null, hasSecurityFile: pkg.hasSecurityFile, @@ -67,6 +75,7 @@ export async function getPackage(req: Request, res: Response): Promise { openSSFScorecard: scorecardScore, }, }, + signalCoverageHealth: pkg.signalCoverageHealth, assessment: null, security: { securityContacts: null, diff --git a/backend/src/api/public/v1/packages/openapi.yaml b/backend/src/api/public/v1/packages/openapi.yaml index b06a0cd291..cd5776693f 100644 --- a/backend/src/api/public/v1/packages/openapi.yaml +++ b/backend/src/api/public/v1/packages/openapi.yaml @@ -267,10 +267,27 @@ components: type: object properties: healthScore: + type: + - integer + - 'null' + description: Composite health score (0–100). Tinybird-enriched when available, null otherwise. + example: 18 + healthScoreDetails: type: - object - 'null' + description: Breakdown of the composite health score. Null until Tinybird enrichment runs. properties: + total: + type: + - integer + - 'null' + example: 18 + label: + type: + - string + - 'null' + example: Good maintainerHealth: type: - integer @@ -286,11 +303,13 @@ components: - integer - 'null' example: 6 - total: - type: - - integer - - 'null' - example: 18 + healthBand: + type: + - string + - 'null' + enum: [healthy, fair, concerning, critical, null] + description: Derived from Tinybird health score when available, OpenSSF Scorecard otherwise. + example: concerning impact: type: - object @@ -466,6 +485,12 @@ components: history: type: object description: Package history data. Empty in v1. + signalCoverageHealth: + type: + - object + - 'null' + description: Signal coverage health data enriched by Tinybird. Null until enriched. + additionalProperties: true # ── Metrics ────────────────────────────────────────────────────────────────── @@ -672,11 +697,14 @@ paths: name: lodash ecosystem: npm general: - healthScore: + healthScore: 18 + healthScoreDetails: + total: 18 + label: critical maintainerHealth: 4 securitySupplyChain: 8 developmentActivity: 6 - total: 18 + healthBand: critical impact: impactScore: 71 downloadsLastMonth: 52142891 diff --git a/services/libs/data-access-layer/src/osspckgs/api.ts b/services/libs/data-access-layer/src/osspckgs/api.ts index a1d3fa7a60..0d1e3fdd6c 100644 --- a/services/libs/data-access-layer/src/osspckgs/api.ts +++ b/services/libs/data-access-layer/src/osspckgs/api.ts @@ -618,6 +618,14 @@ export interface PackageDetailRow { downloadsLast30d: string | null maintainerCount: number transitiveReach: number | null + // Tinybird-enriched health fields + healthScore: number | null + healthLabel: string | null + maintainerHealthScore: number | null + securitySupplyChainScore: number | null + developmentActivityScore: number | null + lifecycleLabel: string | null + signalCoverageHealth: Record | null } export interface AdvisoryRow { @@ -672,6 +680,13 @@ export async function getPackageDetailByPurl( LIMIT 1 ) AS "downloadsLast30d", (SELECT COUNT(*)::int FROM package_maintainers pm WHERE pm.package_id = p.id) AS "maintainerCount", + p.health_score AS "healthScore", + p.health_label AS "healthLabel", + p.maintainer_health_score AS "maintainerHealthScore", + p.security_supply_chain_score AS "securitySupplyChainScore", + p.development_activity_score AS "developmentActivityScore", + p.lifecycle_label AS "lifecycleLabel", + p.signal_coverage_health AS "signalCoverageHealth", -- TODO: precompute and store in packages.transitive_reach_prank; full window scan is too slow at npm scale (~24s for npm) -- ( -- SELECT r.prank