feat: add nuget ecosystem to blast (CM-1358) - #4443
Conversation
There was a problem hiding this comment.
Pull request overview
Adds NuGet support to the blast-radius analysis pipeline.
Changes:
- Adds NuGet intel, dependent scanning, source resolution, and reachability analysis.
- Introduces shared ecosystem-version handling and NuGet constraint parsing.
- Extends API validation, dispatch, prompts, and tests for NuGet.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
backend/src/api/public/v1/packages/blastRadius.ts |
Allows NuGet requests. |
services/apps/packages_worker/src/nuget/client.ts |
Adds version-list fetching. |
services/apps/packages_worker/src/blast-radius/ecosystemSupport.ts |
Registers NuGet support. |
services/apps/packages_worker/src/blast-radius/packageIdentifier.ts |
Normalizes NuGet identifiers. |
services/apps/packages_worker/src/blast-radius/__tests__/packageIdentifier.test.ts |
Tests NuGet identifiers. |
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts |
Adds C# analysis prompts. |
services/apps/packages_worker/src/blast-radius/clients/nugetSource.ts |
Resolves GitHub source archives. |
services/apps/packages_worker/src/blast-radius/clients/__tests__/nugetSource.test.ts |
Tests source resolution. |
services/apps/packages_worker/src/blast-radius/clients/osvClient.ts |
Simplifies duplicate merging. |
services/apps/packages_worker/src/blast-radius/stages/ecosystemVersions.ts |
Shares ecosystem range logic. |
services/apps/packages_worker/src/blast-radius/stages/__tests__/ecosystemVersions.test.ts |
Tests shared version logic. |
services/apps/packages_worker/src/blast-radius/stages/ecosystems.ts |
Registers NuGet stage handlers. |
services/apps/packages_worker/src/blast-radius/stages/__tests__/dispatch.test.ts |
Tests NuGet dispatch. |
services/apps/packages_worker/src/blast-radius/stages/selectAdvisoryEntry.ts |
Defers affected-name construction. |
services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts |
Implements NuGet intel. |
services/apps/packages_worker/src/blast-radius/stages/nuget/dependentsNuGet.ts |
Persists NuGet dependents. |
services/apps/packages_worker/src/blast-radius/stages/nuget/dependentsScanNuGet.ts |
Scans reverse dependencies. |
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts |
Configures source analysis. |
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetConstraint.ts |
Parses dependency constraints. |
services/apps/packages_worker/src/blast-radius/stages/nuget/__tests__/nugetConstraint.test.ts |
Tests constraint matching. |
services/apps/packages_worker/src/blast-radius/stages/maven/mavenVersions.ts |
Uses shared version utilities. |
services/apps/packages_worker/src/blast-radius/stages/maven/intelMaven.ts |
Updates request handling. |
services/apps/packages_worker/src/blast-radius/stages/go/intelGo.ts |
Updates request handling. |
services/apps/packages_worker/src/blast-radius/stages/npm/intelNpm.ts |
Updates request handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
caea4df to
07a1485
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (7)
services/apps/packages_worker/src/blast-radius/stages/npm/intelNpm.ts:64
package_nameisstring | null, so an advisory-wide request reachestoBareNpmName(null)and throws beforeselectAdvisoryEntrycan handle the omitted package. Check for a string rather than only excludingundefined; this also preserves the explicit empty-string behavior.
const requestedPackage =
analysisDetail?.package_name !== undefined ? toBareNpmName(analysisDetail.package_name) : null
services/apps/packages_worker/src/blast-radius/stages/go/intelGo.ts:67
package_nameisstring | null, so an advisory-wide request passesnulltotoBareGoModuleand fails on.trim(). Narrow to a string before normalization while retaining empty strings as explicit requests.
const requestedModule =
analysisDetail?.package_name !== undefined
? toBareGoModule(analysisDetail.package_name)
: null
services/apps/packages_worker/src/blast-radius/stages/maven/intelMaven.ts:71
package_nameis nullable inAnalysisDetailRow; checking onlyundefinedsendsnullintotoBareMavenCoordinate, breaking advisory-wide Maven analyses. Narrow to a string before normalizing.
const requested =
analysisDetail?.package_name !== undefined
? toBareMavenCoordinate(analysisDetail.package_name)
: null
services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts:66
- For an advisory-wide NuGet request,
package_nameisnull, notundefined; this therefore callstoBareNuGetId(null)and the intel stage fails before selecting the sole advisory entry. Narrow to a string before normalization.
const requestedId =
analysisDetail?.package_name !== undefined ? toBareNuGetId(analysisDetail.package_name) : null
services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts:74
- NuGet package IDs are case-insensitive (the existing NuGet client already case-folds IDs), but this exact comparison rejects valid requests such as
newtonsoft.jsonwhen OSV usesNewtonsoft.Json. Match case-insensitively and then use the OSV entry's canonical spelling for the case-sensitive DB lookup.
(e) => e.package.name === requestedId,
advisoryOsvId,
)
const nugetId = requestedId ?? entry.package.name
services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts:86
- The promised DB fallback is bypassed for actual network failures and NuGet 5xx responses:
fetchRegistrationonly converts 404/429 intoNuGetFetchErrorand rethrows other Axios errors. Catch those failures here (or classify them in the client) and loadgetVersionNumberswhendbPackageIdexists.
// The nuget.org registration index is the authoritative version list; fall back to
// our own ingested `versions` rows (deps.dev) if the registry is unreachable/rate-limited
// and the package is already known to us.
const versionListResult = await fetchVersionList(nugetId)
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts:13
- This states that
dep.versionis always null, butscanNuGetDependentspersistsrow.versionNumber, which comes from an inner-joined dependentversionsrow and is non-null. The code therefore uses that concrete dependent version; describe the highest-version lookup as a defensive fallback instead.
// deps.dev never resolves a concrete version for NuGet edges (see dependentsScanNuGet.ts),
// so dep.version is always null — fall back to the package's current highest listed version.
PR SummaryMedium Risk Overview NuGet-specific behavior includes ECOSYSTEM-typed OSV ranges and a dedicated four-part version comparator (shared with refactored Maven range logic in Reviewed by Cursor Bugbot for commit 32b0bcb. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:53
- This section-header comment violates the repository rule that explicitly disallows section headers; the exported prompt names already make this boundary clear. Remove it.
// ---------- STAGE 3: REACHABILITY ----------
services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts:83
- This exact-name lookup breaks NuGet's case-insensitive identity semantics.
findPackageIdusesname = $(name), and real GHSA/OSV package casing can differ from the NuGet Gallery/DB casing; then stage 1 stores a null package ID and stage 2 fails even though the registry fetch succeeded. Resolve the registry's canonical ID first or add a NuGet-specific case-insensitive DAL lookup.
const dbPackageId = await findPackageId(qx, { ecosystem, namespace: null, name: nugetId })
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetConstraint.ts:104
- Returning
trueconverts a comparison failure intomatched, rather thanunparseable-included. For example, the bare constraint>=1.0passes the floor parser, fails version comparison, and is persisted as a successful match, contradicting the result contract and diagnostics. Propagate an indeterminate result and returnunparseable-includedwhen no definite match exists but a comparison failed.
if (c === null) return true // unparseable bound — over-inclusive
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts:25
- This fallback is dead in the NuGet pipeline:
dependentsScanNuGet.ts:38storesrow.versionNumber(the dependent package's own concrete version) intodep.version. The missing deps.dev value is the dependency edge's resolved target version, not the dependent source version. Remove the unused registry fallback and correct this explanation so these two concepts are not conflated.
const version = dep.version ?? (await resolveNuGetVersion(dep.name))
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:11
- This section-header comment violates the repository rule that explicitly disallows section headers; the schema names already provide the structure. Remove it.
This issue also appears on line 53 of the same file.
// ---------- STAGE 1: INTEL ----------
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a8a5b0f. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:53
- Remove this section-header comment. Project guidance explicitly prohibits section headers in code (
CLAUDE.md:82); the constant name already identifies this block.
// ---------- STAGE 3: REACHABILITY ----------
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetVersionCompare.ts:51
- NuGet's official
VersionComparercompares nonnumeric prerelease labels withOrdinalIgnoreCase. This case-sensitive comparison can falsely exclude equivalent versions at range boundaries (for example,[1.0.0-BETA]versus1.0.0-beta). Normalize both labels before ordering and add a regression test.
if (aIsNum !== bIsNum) return aIsNum ? -1 : 1
if (aParts[i] !== bParts[i]) return aParts[i] < bParts[i] ? -1 : 1
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:13
- Remove this section-header comment. Project guidance explicitly prohibits section headers in code (
CLAUDE.md:82); the constant name already identifies this block.
This issue also appears on line 53 of the same file.
// ---------- STAGE 1: INTEL ----------
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts:14
- This comment is factually incorrect:
scanNuGetDependentsstoresrow.versionNumberincandidate.version(dependentsScanNuGet.ts:38), sodep.versionnormally contains the selected dependent version. The registry fallback only applies to null/legacy records; remove the misleading statement.
// deps.dev never resolves a concrete version for NuGet edges (see dependentsScanNuGet.ts),
// so dep.version is always null — fall back to the package's current highest listed version.
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (5)
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts:25
- This fallback is unreachable in the new pipeline:
scanNuGetDependentsalways setsversionfromrow.versionNumber, which is guaranteed bygetReverseDependents' inner join and then persisted. Therefore the implementation never performs the PR-described “highest listed version” fallback, andfetchVersionListis dead on this path. Either use registry resolution when that behavior is intended, or remove the fallback and document that the concrete dependent version from deps.dev is analyzed.
const version = dep.version ?? (await resolveNuGetVersion(dep.name))
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:11
- Section-header comments are explicitly disallowed by the project guidance. The exported schema and prompt names already make this boundary clear, so remove this header.
// ---------- STAGE 1: INTEL ----------
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetVersionCompare.ts:51
- NuGet compares non-numeric prerelease labels case-insensitively (
OrdinalIgnoreCase), but this comparison is case-sensitive. As a result, versions such as1.0.0-Betaand1.0.0-betaare ordered differently instead of comparing equal, which can misclassify OSV range boundaries and select the wrong highest version. Normalize both labels before comparing and add a mixed-case regression test.
if (aParts[i] !== bParts[i]) return aParts[i] < bParts[i] ? -1 : 1
services/apps/packages_worker/src/nuget/client.ts:151
- This comment only restates that the wrapper returns version strings, which is already clear from
fetchVersionListand its return type. Project guidance explicitly disallows comments that merely describe what code does; remove it.
// Thin wrapper for callers (blast-radius intel) that only need the version strings,
// not the full registration payload with per-version metadata.
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:53
- Section-header comments are explicitly disallowed by the project guidance. The following reachability symbols are self-describing, so remove this header.
// ---------- STAGE 3: REACHABILITY ----------
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (9)
services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts:83
- This lookup is case-sensitive (
findPackageIdusesname = $(name)), while NuGet IDs are case-insensitive andnugetIdcomes from OSV rather than the packages row. If OSV and deps.dev preserve different casing,package_idremains null and the dependents stage fails with “package_id not resolved.” Resolve NuGet package IDs case-insensitively through an indexed DAL lookup while preserving canonical display casing.
const dbPackageId = await findPackageId(qx, { ecosystem, namespace: null, name: nugetId })
services/apps/packages_worker/src/blast-radius/stages/nuget/dependentsScanNuGet.ts:35
- NuGet requirements are recorded per target framework, but the ingestion feeding this row collapses duplicate
(root, dependency)entries: the full query flattens them and downstream deduplication keeps one, while the incremental query usesMAX(dep.Requirement)(deps-dev/queries/depsSql.ts:25-37,199-211). A dependency can therefore be excluded here even though another target framework has a range containing the vulnerable version. Preserve and evaluate every distinct requirement before filtering candidates.
const rangeCheck = nugetConstraintMayInclude(row.versionConstraint, vulnerableVersions)
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetVersionCompare.ts:51
- NuGet.Versioning compares non-numeric prerelease labels with ordinal, case-insensitive semantics. This case-sensitive branch treats
1.0.0-betaand1.0.0-BETAas different, which can misclassify range boundaries and choose the wrong highest version.
if (aParts[i] !== bParts[i]) return aParts[i] < bParts[i] ? -1 : 1
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts:13
- This comment is incorrect:
scanNuGetDependentsstoresrow.versionNumberindep.version, so normal NuGet rows do not always reach this fallback. Remove the misleading explanation; the fallback only covers a missing persisted dependent version.
// deps.dev never resolves a concrete version for NuGet edges (see dependentsScanNuGet.ts),
// so dep.version is always null — fall back to the package's current highest listed version.
backend/src/api/public/v1/packages/blastRadius.ts:3
- Add a schema test that submits
ecosystem: 'nuget'. This public API enum has dedicated acceptance/rejection tests inblastRadius.test.ts, but the newly exposed value is not covered there.
export const SUPPORTED_BLAST_RADIUS_ECOSYSTEMS = ['npm', 'go', 'maven', 'cargo', 'nuget'] as const
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetConstraint.ts:66
- Validate both parsed bounds before accepting the interval. Currently a partially malformed constraint such as
[2.0,garbage]can exclude vulnerable version1.5on the valid lower bound before the invalid upper bound is noticed, contradicting the promisedunparseable-includedbehavior.
const lower = body.slice(0, comma).trim() || null
const upper = body.slice(comma + 1).trim() || null
return { lower, lowerInclusive, upper, upperInclusive }
services/apps/packages_worker/src/nuget/client.ts:151
- This comment only restates the wrapper’s name, return type, and implementation. Remove it to follow the repository rule against comments that explain what obvious code does (
CLAUDE.md:72-83).
// Thin wrapper for callers (blast-radius intel) that only need the version strings,
// not the full registration payload with per-version metadata.
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetVersionCompare.ts:5
- This external-compatibility explanation exceeds the repository’s two-line limit for permitted comments and does not link the API quirk (
CLAUDE.md:72-84). Condense it and link the NuGet versioning behavior.
// node-semver rejects any version with a 4th numeric component (e.g. "1.2.3.4"), but
// NuGetVersion accepts Major.Minor.Patch.Revision — common in Microsoft/BCL packages
// (System.*, Microsoft.NETCore.*). Routing NuGet through the shared semver comparator
// silently drops those versions from range checks; this is a NuGetVersion-compatible
// comparator scoped to blast-radius so it doesn't change the shared OSV sync pipeline.
services/apps/packages_worker/src/blast-radius/stages/tests/ecosystemVersions.test.ts:48
- This explanation reverses the ordering in its first clause. NuGet’s numeric component comparison places
1.10.0after1.9.0; rewrite the comment so it does not contradict the assertion.
// Semver orders 1.9.0 > 1.10.0's minor is 10 > 9, so unlike the Maven case above,
// 1.10.0 must sort after 1.9.0 here too, but via a different comparator entirely.
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (6)
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:53
- Remove this section-header comment. The repository convention explicitly prohibits section-header comments (
CLAUDE.md:72-84); theIMPORT_STYLE_ENUMdeclaration already identifies the reachability section.
// ---------- STAGE 3: REACHABILITY ----------
services/apps/packages_worker/src/blast-radius/stages/nuget/nugetVersionCompare.ts:51
- NuGet compares non-numeric prerelease labels with
OrdinalIgnoreCase, but this comparison is case-sensitive. An exact range such as[1.0.0-BETA]therefore excludes vulnerable version1.0.0-beta, creating a false negative. Compare the ASCII labels case-insensitively and add that case to the regression tests.
if (aParts[i] !== bParts[i]) return aParts[i] < bParts[i] ? -1 : 1
services/apps/packages_worker/src/blast-radius/stages/nuget/reachabilityConfig.ts:25
- This fallback is unreachable for NuGet dependents:
scanNuGetDependentsalways assigns the non-nullrow.versionNumber, and that value is persisted asdep.version. Consequently the implementation does not fall back to the registry's highest listed version as described. Either passnullwhen that fallback is intended, or remove this resolver and update the description/comments to reflect that the latest ingested dependent version is analyzed.
const version = dep.version ?? (await resolveNuGetVersion(dep.name))
backend/src/api/public/v1/packages/blastRadius.ts:3
- Add a schema regression test that parses a request with
ecosystem: 'nuget'.backend/src/api/public/v1/packages/blastRadius.test.tsalready contracts accepted and rejected ecosystem values, but this new public API value is currently only tested in the worker's separate support list.
export const SUPPORTED_BLAST_RADIUS_ECOSYSTEMS = ['npm', 'go', 'maven', 'cargo', 'nuget'] as const
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:13
- Remove this section-header comment. The repository convention explicitly prohibits section-header comments (
CLAUDE.md:72-84); theIMPORT_SIGNATURE_KEYSdeclaration already makes the section self-explanatory.
This issue also appears on line 53 of the same file.
// ---------- STAGE 1: INTEL ----------
services/apps/packages_worker/src/blast-radius/agent/nugetPrompts.ts:25
- The public API now accepts every NuGet package, while this prompt assumes all packages are C#. NuGet also distributes F#, VB, and other .NET-language packages; those advisories would produce the wrong symbol/import patterns and risk false
not_affectedverdicts. Either detect the source language and select suitable prompts or validate/reject unsupported packages.
export const NUGET_INTEL_SYSTEM_PROMPT = `You are a vulnerability analyst. Your working directory contains the source (fetched from the
package's GitHub repository at the matching commit/tag) of the vulnerable version of a NuGet
package (C#/.NET). You are given the security advisory and the patch (diff) that fixed the
vulnerability.

Summary
Adds NuGet as the fifth supported ecosystem in the blast-radius pipeline (after npm, Go,
Maven and Cargo), so advisories affecting NuGet packages can be analyzed end-to-end:
intel → dependents → reachability.
NuGet has two quirks the existing ecosystems don't: OSV spells the ecosystem
NuGet(mixed case) while our DB stores
nuget, and deps.dev never resolves a concrete versionfor NuGet dependency edges — so matching goes purely through
version_constraintand thereachability stage falls back to the dependent's highest listed version.
Changes
blast-radius/stages/nuget/:intelNuGet.ts,dependentsNuGet.ts,dependentsScanNuGet.ts,reachabilityConfig.ts, plus agent prompts inblast-radius/agent/nugetPrompts.ts.nugetConstraint.tsimplements NuGet's interval notation ([1.0,2.0), comma-joinedalternatives). Unmatched brackets or trailing text return
nullrather than a partialparse: accepting a valid-looking prefix could silently drop the tail and wrongly exclude
a vulnerable version, so it falls through to
unparseable-included.clients/nugetSource.tsresolves a dependent's GitHub source from the nuspec<repository>element (url + commit — the registration API never exposes it) andextracts the C# source at that commit for the reachability agent.
nuget/client.ts: addedfetchVersionList— thin wrapper for callers that only needversion strings, not the full registration payload.
nugetinSUPPORTED_ECOSYSTEMS(blast-radius/ecosystemSupport.ts),SUPPORTED_BLAST_RADIUS_ECOSYSTEMS(public API schema) and theECOSYSTEMSdispatchrecord in
stages/ecosystems.ts.toBareNuGetIdinpackageIdentifier.ts. Deliberately does not lowercase:findPackageId/findPackageIdsByNamecompare against canonical casing.Type of change
JIRA ticket
CM-1358