Skip to content

feat(remediation): add remediation extractor with provider priority resolution - #599

Open
ruromero wants to merge 3 commits into
guacsec:mainfrom
ruromero:TC-5410
Open

feat(remediation): add remediation extractor with provider priority resolution#599
ruromero wants to merge 3 commits into
guacsec:mainfrom
ruromero:TC-5410

Conversation

@ruromero

@ruromero ruromero commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds src/remediation.js module exporting extractRemediations(analysisReport) that parses DA AnalysisReport responses and produces actionable upgrade instructions
  • Implements three-tier provider priority resolution: Lightwell (.rhlw-) > Red Hat (.redhat-) > generic
  • For the same dependency affected by multiple CVEs, selects the highest fixedIn version from the highest-priority provider
  • Extracts package replacement recommendations from ProviderReport.recommendations
  • Comprehensive test suite with 14 tests covering priority resolution, version merging, edge cases, and output structure

Implements TC-5410

Summary by Sourcery

Introduce a remediation extraction module that derives prioritized upgrade instructions from DA AnalysisReport data and exposes them as a normalized output structure.

New Features:

  • Add extractRemediations(analysisReport) API to parse analysis reports into actionable remediation entries with package coordinates, target version, severity, CVEs, and advisories.
  • Implement provider priority handling so Lightwell rebuilt packages override Red Hat fixes, which in turn override generic recommendations for the same dependency.
  • Support remediation extraction from both issue-level fixedIn/trustedContent data and provider-level package replacement recommendations.

Tests:

  • Add a comprehensive remediation.test.js suite covering provider priority behavior, version merging across multiple CVEs, edge cases such as missing data, recommendation precedence, and output schema validation.

@sourcery-ai

sourcery-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a remediation extraction pipeline that walks DA AnalysisReport provider/source/dependency/issue trees, resolves provider priority (Lightwell > Red Hat > generic), merges multiple CVE remediations per dependency to a single highest-version entry, incorporates recommendation-based package replacements, and returns a normalized remediation list suitable for consumption by downstream tooling.

Flow diagram for remediation extraction and provider priority resolution

flowchart TD
  analysisReport[AnalysisReport]
  extractRemediations[extractRemediations]
  providersLoop[Iterate providers]
  extractFromSources[extractFromSources]
  extractFromRecommendations[extractFromRecommendations]
  processIssueRemediation[processIssueRemediation]
  detectProviderPriority[detectProviderPriority]
  compareVersions[compareVersions]
  remediationsByDep[remediationsByDep Map]
  outputList[Sorted remediation list]

  analysisReport --> extractRemediations
  extractRemediations --> providersLoop
  providersLoop --> extractFromSources
  providersLoop --> extractFromRecommendations

  extractFromSources --> processIssueRemediation
  processIssueRemediation --> detectProviderPriority
  processIssueRemediation --> compareVersions
  processIssueRemediation --> remediationsByDep

  extractFromRecommendations --> detectProviderPriority
  detectProviderPriority --> remediationsByDep
  extractFromRecommendations --> remediationsByDep

  remediationsByDep --> outputList
Loading

File-Level Changes

Change Details Files
Add remediation extraction module that normalizes DA AnalysisReport data into a sorted list of upgrade instructions.
  • Introduce extractRemediations(analysisReport) as the main entry point returning structured remediation objects.
  • Validate analysisReport/providers presence and early-return with empty array on null or malformed input.
  • Accumulate per-dependency remediation data in a Map and strip internal _priority metadata before returning results.
  • Sort final remediation entries by dependency PURL for deterministic, idempotent output.
src/remediation.js
Implement provider/source tree traversal and issue-level remediation handling with provider priority and version merging.
  • Traverse providerReport.sources and dependencies, skipping missing dependencies/issues defensively.
  • Derive fixedIn PURLs from issue.remediation, preferring trustedContent.ref over fixedIn fields.
  • Parse dependency and fixed-in PURLs via packageurl-js and derive groupId, artifactId, currentVersion, and fixedInVersion.
  • Compute provider priority from fixed-in PURL suffixes (.rhlw- for Lightwell, .redhat- for Red Hat, otherwise generic).
  • Merge multiple issues for the same dependency by combining CVE lists, deduplicating advisories, and keeping highest severity and highest fixedInVersion within the winning provider priority tier.
src/remediation.js
Support recommendation-based package replacements and integrate them into the same remediation selection logic.
  • Read providerReport.recommendations.dependencies and extract recommendation refs as fixed-in PURLs.
  • Parse recommendation PURLs and generate remediation entries with source='recommendation' and default UNKNOWN severity when no issue data exists.
  • Apply the same priority detection to recommendations and allow higher-priority recommendation entries to override existing source-based remediations for the same dependency.
  • Skip invalid or non-string recommendation refs, or entries lacking versions, to avoid malformed output.
src/remediation.js
Implement helper utilities for advisory merging, version comparison, and severity ranking to support correct prioritization.
  • Extract advisories from both issue.remediation.trustedContent.advisory and issue.advisories into a normalized {id, url} array.
  • Merge advisories across issues while deduplicating by advisory id.
  • Compare semantic-ish version strings segment-by-segment to choose the highest fixedInVersion when priorities are equal.
  • Define an ordered severity scale (UNKNOWN < LOW < MEDIUM < HIGH < CRITICAL) and select the higher severity when merging multiple issues.
src/remediation.js
Add comprehensive unit tests covering provider priority resolution, version merging, edge cases, recommendations, and output schema.
  • Create buildReport() test helper to construct minimal and customized AnalysisReport fixtures.
  • Test Lightwell vs Red Hat vs generic provider precedence for the same dependency and verify trustedContent vs fixedIn handling.
  • Verify merging behavior for multiple CVEs on the same dependency, including highest-version selection and highest-severity preservation.
  • Cover edge cases such as null/undefined input, empty providers, missing remediation data, and idempotent behavior.
  • Test recommendation extraction, precedence of source-based remediation over recommendations when higher priority, and ensure output objects expose all required fields without internal _priority.
test/remediation.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The compareVersions helper assumes purely dot-separated numeric segments, which may not correctly order versions with qualifiers like 1.10.0.redhat-00001; consider using a more robust Maven/semver-aware comparison or a fallback that preserves qualifier significance.
  • Provider priority detection in detectProviderPriority is based on substring matches (.rhlw-, .redhat-), which could misclassify PURLs if those tokens appear outside the version qualifier; you may want to inspect the parsed PackageURL.version or qualifiers instead of the raw string.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `compareVersions` helper assumes purely dot-separated numeric segments, which may not correctly order versions with qualifiers like `1.10.0.redhat-00001`; consider using a more robust Maven/semver-aware comparison or a fallback that preserves qualifier significance.
- Provider priority detection in `detectProviderPriority` is based on substring matches (`.rhlw-`, `.redhat-`), which could misclassify PURLs if those tokens appear outside the version qualifier; you may want to inspect the parsed `PackageURL.version` or qualifiers instead of the raw string.

## Individual Comments

### Comment 1
<location path="src/remediation.js" line_range="301-302" />
<code_context>
+ * @param {string} b
+ * @returns {number}
+ */
+function compareVersions(a, b) {
+	const partsA = a.split('.').map(s => parseInt(s, 10) || 0)
+	const partsB = b.split('.').map(s => parseInt(s, 10) || 0)
+	const len = Math.max(partsA.length, partsB.length)
</code_context>
<issue_to_address>
**issue (bug_risk):** Version comparison treats any non-numeric segment as 0, which can misorder common semver patterns.

Non-numeric segments (e.g. `1.2.3-beta`, `2024.01-1`) are coerced to `0`, which can produce incorrect ordering or equality and may select the wrong fixed version when qualifiers are present. Please either use a semver/Maven-style comparator where appropriate, or keep the raw segments and apply a comparison that handles non-numeric parts lexically instead of collapsing them to `0`.
</issue_to_address>

### Comment 2
<location path="src/remediation.js" line_range="209-214" />
<code_context>
+				cves: [],
+				_priority: priority,
+			})
+		} else if (priority > existing._priority) {
+			existing.fixedInVersion = fixedInVersion
+			existing.fixedInPurl = recommendedPurl
+			existing.provider = providerName
+			existing.source = 'recommendation'
+			existing._priority = priority
+		}
+	}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Recommendations only override existing entries on strictly higher priority, ignoring equal-priority but higher versions.

In `extractFromRecommendations`, entries with `priority === existing._priority` are skipped entirely, even when they have a higher `fixedInVersion`. In contrast, `processIssueRemediation` compares versions for equal-priority entries to keep the highest one. To avoid missing better remediations and to keep behavior consistent, update `extractFromRecommendations` to also compare versions when priorities are equal.
</issue_to_address>

### Comment 3
<location path="test/remediation.test.js" line_range="188-190" />
<code_context>
+		})
+	})
+
+	suite('version merging', () => {
+		/** Verifies that multiple CVEs on the same dependency produce a single entry with the highest fix version. */
+		test('multiple CVEs on same dependency produce single entry with highest version', () => {
+			// Given a dependency with two CVEs having different fix versions from the same provider
+			const report = buildReport({
</code_context>
<issue_to_address>
**suggestion (testing):** Add test coverage for advisory merging across multiple issues on the same dependency

The code merges advisories via `mergeAdvisories`, deduping by `id`, but current tests only cover a single-issue case. Please add a test where the same dependency appears in multiple issues with distinct advisories (e.g., one from `trustedContent.advisory` and one from `issue.advisories`) and assert that the resulting `advisories` array is the deduplicated union. This will better validate the multi-CVE advisory merge behavior.

Suggested implementation:

```javascript
			// When extracting remediations
			const result = extractRemediations(report)

			// Then Red Hat should win
			expect(result).to.have.lengthOf(1)
			expect(result[0].fixedInPurl).to.include('.redhat-')
			expect(result[0].provider).to.equal('redhat')
		})

		/** Verifies that advisories from multiple issues and trusted content merge into a deduplicated union. */
		test('advisories from multiple issues on same dependency are merged and deduplicated', () => {
			// Given the same dependency referenced in multiple issues and trusted content,
			// with overlapping advisory IDs
			const report = buildReport({
				issues: [
					{
						id: 'issue-1',
						dependencyRef: 'pkg:maven/org.apache.commons/commons-text@1.9.0?type=jar',
						advisories: [
							{
								id: 'ADV-ISSUE-1',
								url: 'https://example.com/advisory/ADV-ISSUE-1',
							},
							{
								id: 'ADV-SHARED',
								url: 'https://example.com/advisory/ADV-SHARED-from-issue',
							},
						],
					},
					{
						id: 'issue-2',
						dependencyRef: 'pkg:maven/org.apache.commons/commons-text@1.9.0?type=jar',
						advisories: [
							{
								id: 'ADV-ISSUE-2',
								url: 'https://example.com/advisory/ADV-ISSUE-2',
							},
						],
					},
				],
				trustedContent: [
					{
						providerName: 'lightwell',
						sourceName: 'lightwell-security',
						trustedContentRef:
							'pkg:maven/org.apache.commons/commons-text@1.9.0.rhlw-00001?type=jar',
						fixedIn: null,
						// One unique advisory and one that shares the same id as an issue advisory
						advisories: [
							{
								id: 'ADV-TRUSTED-1',
								url: 'https://lightwell.example.com/ADV-TRUSTED-1',
							},
							{
								id: 'ADV-SHARED',
								url: 'https://lightwell.example.com/ADV-SHARED-from-trusted',
							},
						],
					},
				],
			})

			// When extracting remediations
			const result = extractRemediations(report)

			// Then the advisories for the commons-text remediation should be the deduplicated union by id
			expect(result).to.have.lengthOf(1)

			const advisories = result[0].advisories
			const advisoryIds = advisories.map((a) => a.id)

			// We expect three distinct advisory IDs:
			// - ADV-ISSUE-1 (issue-only)
			// - ADV-ISSUE-2 (issue-only)
			// - ADV-TRUSTED-1 (trusted-content-only)
			// The overlapping ADV-SHARED should only appear once
			expect(advisoryIds).to.have.members(['ADV-ISSUE-1', 'ADV-ISSUE-2', 'ADV-TRUSTED-1', 'ADV-SHARED'])
			expect(advisories).to.have.lengthOf(4)
		})
	})

```

The above test assumes:
1. `buildReport` accepts `issues` and `trustedContent` properties where:
   - `issues[*].dependencyRef` matches the dependency coordinate used elsewhere in the tests.
   - `issues[*].advisories` is an array of advisory objects.
   - `trustedContent[*].advisories` is an array (not a single `advisory` object). If your current trusted-content model uses a singular `advisory` field, adjust the fixture to match (e.g. change `advisories` to `advisory` and adapt how `mergeAdvisories` consumes it).
2. `extractRemediations(report)` returns an array of remediation objects where `remediation.advisories` is the merged advisory array for that dependency. If the property name differs (e.g. `mergedAdvisories` or `details.advisories`), update the test accordingly.
3. If the dependency-identifying property is not `dependencyRef` (e.g. `purl` or `dependency`), change `dependencyRef` in the fixture to the correct field name so all three records clearly refer to the same dependency.

Please align the field names in the new test with your existing `buildReport` and `mergeAdvisories` implementations to ensure the test accurately exercises the advisory-merge path.
</issue_to_address>

### Comment 4
<location path="test/remediation.test.js" line_range="361-363" />
<code_context>
+		})
+	})
+
+	suite('output structure', () => {
+		/** Verifies that the output includes all required fields with correct types. */
+		test('output includes all required fields', () => {
+			const report = buildReport({
+				trustedContentRef: 'pkg:maven/org.apache.commons/commons-text@1.10.0.rhlw-00001?type=jar',
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for defaulted and alternative fields (severity default, issue.cve, and sorting by purl)

The current test covers field presence and types but not key behaviours: defaulting `severity` to `UNKNOWN` when missing, handling CVE identifiers via both `issue.id` and `issue.cve`, and sorting results by `purl`. Please add targeted tests for each of these cases to fully validate `extractRemediations`’ behaviour.

Suggested implementation:

```javascript
				},
			})

			const result = extractRemediations(report)

			expect(result).to.have.lengthOf(1)
			expect(result[0].fixedInPurl).to.include('.redhat-')
		})

		test('defaults missing severity to UNKNOWN', () => {
			const report = buildReport({
				trustedContentRef: 'pkg:maven/org.example/foo@1.0.0?type=jar',
				// Intentionally omit severity to verify defaulting behaviour
				severity: undefined,
				fixedIn: '1.0.1',
				advisory: { id: 'RHLW-2022-002', url: 'https://lightwell.example.com/RHLW-2022-002' },
			})

			const result = extractRemediations(report)

			expect(result).to.have.lengthOf(1)
			expect(result[0].severity).to.equal('UNKNOWN')
		})

		test('prefers issue.cve over issue.id when present', () => {
			const report = buildReport({
				trustedContentRef: 'pkg:maven/org.example/bar@2.0.0?type=jar',
				severity: 'HIGH',
				fixedIn: '2.0.1',
				issue: {
					// Both id and cve present – cve should be used as the canonical identifier
					id: 'LIGHTWELL-2022-003',
					cve: 'CVE-2022-0003',
				},
				advisory: { id: 'RHLW-2022-003', url: 'https://lightwell.example.com/RHLW-2022-003' },
			})

			const result = extractRemediations(report)

			expect(result).to.have.lengthOf(1)
			// The remediation should expose the CVE identifier when available
			expect(result[0].issueId).to.equal('CVE-2022-0003')
		})

		test('results are sorted by purl', () => {
			const report = buildReport({
				// Provide multiple remediations with deliberately unsorted purls
				remediations: [
					{
						trustedContentRef: 'pkg:maven/org.example/zzz@3.0.0?type=jar',
						severity: 'LOW',
						fixedIn: '3.0.1',
						issue: { id: 'ISSUE-3' },
						advisory: { id: 'RHLW-2022-004', url: 'https://lightwell.example.com/RHLW-2022-004' },
					},
					{
						trustedContentRef: 'pkg:maven/org.example/aaa@1.0.0?type=jar',
						severity: 'CRITICAL',
						fixedIn: '1.0.1',
						issue: { id: 'ISSUE-1' },
						advisory: { id: 'RHLW-2022-005', url: 'https://lightwell.example.com/RHLW-2022-005' },
					},
					{
						trustedContentRef: 'pkg:maven/org.example/mmm@2.0.0?type=jar',
						severity: 'MEDIUM',
						fixedIn: '2.0.1',
						issue: { id: 'ISSUE-2' },
						advisory: { id: 'RHLW-2022-006', url: 'https://lightwell.example.com/RHLW-2022-006' },
					},
				],
			})

			const result = extractRemediations(report)

			expect(result).to.have.lengthOf(3)
			const purls = result.map(r => r.purl)

			// Verify that the output is sorted lexicographically by purl
			expect(purls).to.deep.equal([...purls].sort())
		})
	})

```

The new tests assume the following about the existing helpers and data structures:
1. `buildReport` can accept:
   - A flat object with `trustedContentRef`, `severity`, `fixedIn`, `issue`, and `advisory` to represent a single remediation.
   - An object with `remediations: [...]` when testing multiple entries. If the current helper uses a different shape (e.g. `vulnerabilities`, `issues`, or `components`), adapt the arguments so that `extractRemediations` receives the intended inputs.
2. `extractRemediations` returns objects that include:
   - `severity` (string), which should be `'UNKNOWN'` when no severity is provided in the source data.
   - `issueId` (string), representing the chosen identifier; update the assertion to match the actual property name if it differs (e.g. `id`, `cve`, or `identifier`).
   - `purl` (string) used for sorting; if the field is named differently (e.g. `packageUrl`), adjust the mapping in the sorting test accordingly.
3. If `buildReport` does not currently support constructing multiple remediations via a `remediations` array, you may need to:
   - Extend the helper to accept and merge such arrays into the underlying report structure in the same way your production data is handled.
   - Or alternatively, create multiple separate reports and merge their `extractRemediations` outputs before asserting sort order.
Ensure these tests are placed within the same `suite('output structure', ...)` block as the existing "output includes all required fields" test to keep related behaviour grouped together.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/remediation.js
Comment thread src/remediation.js Outdated
Comment thread test/remediation.test.js
Comment thread test/remediation.test.js
@codecov-commenter

codecov-commenter commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.18129% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.97%. Comparing base (de62188) to head (955a184).

Files with missing lines Patch % Lines
src/remediation.js 89.18% 37 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #599      +/-   ##
==========================================
- Coverage   91.04%   90.97%   -0.08%     
==========================================
  Files          40       41       +1     
  Lines        8635     8977     +342     
  Branches     1501     1573      +72     
==========================================
+ Hits         7862     8167     +305     
- Misses        773      810      +37     
Flag Coverage Δ
unit-tests 90.97% <89.18%> (-0.08%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/remediation.js 89.18% <89.18%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ruromero
ruromero requested a review from a-oren July 31, 2026 09:44
@ruromero

Copy link
Copy Markdown
Collaborator Author

[sdlc-workflow/verify-pr] Re: @sourcery-ai[bot] review — Classified as suggestion (2 items). Both high-level suggestions (compareVersions numeric coercion and detectProviderPriority substring matching) were already addressed: compareVersions now falls back to lexicographic comparison (commit 6ecff9b), and provider priority detection was replaced with configurable providerPriority option (commit 6ecff9b, removing hardcoded vendor strings entirely). No sub-task created.

@ruromero

Copy link
Copy Markdown
Collaborator Author

Verification Report for TC-5410 (commit 9b7ceeb)

Check Result Details
Review Feedback PASS 2 code change requests (compareVersions, equal-priority recommendations) already fixed in commits 6ecff9b and 9b7ceeb; 2 suggestions remain optional
Root-Cause Investigation N/A No sub-tasks created
Scope Containment PASS PR touches exactly the 2 files specified in task (src/remediation.js, test/remediation.test.js)
Diff Size PASS 795 additions across 2 new files — proportionate for new module + 16 tests
Commit Traceability PASS All 3 commits reference TC-5410
Sensitive Patterns PASS No secrets, credentials, or sensitive data detected
CI Status PASS 5/5 CI checks pass (lint, test Node 22/24, PR title, commit messages)
Acceptance Criteria PASS 7/7 criteria met (provider priority refactored to configurable option — intentional design improvement)
Test Quality PASS 16 tests documented, no repetitive patterns, Eval Quality: N/A
Test Change Classification ADDITIVE test/remediation.test.js is a new file
Verification Commands PASS Lint: 0 errors. Tests: 16/16 pass. c8 coverage threshold failure is pre-existing on main

Overall: PASS

All checks pass. The implementation satisfies the task requirements with an intentional design improvement: provider priority is configurable via options.providerPriority instead of hardcoded vendor-specific detection, making the module suitable for the open-source project.


This comment was AI-generated by sdlc-workflow/verify-pr v0.13.7.

ruromero added 3 commits July 31, 2026 12:14
…esolution

Adds src/remediation.js module that parses DA AnalysisReport responses and
produces actionable upgrade instructions. Implements three-tier provider
priority: Lightwell (.rhlw-) > Red Hat (.redhat-) > generic. For the same
dependency affected by multiple CVEs, selects the highest fixedIn version
from the highest-priority provider. Also extracts package replacement
recommendations. Includes comprehensive test suite.

Implements TC-5410

Assisted-by: Claude Code
… hardcoded

Removes hardcoded vendor-specific priority detection (Red Hat, Lightwell
PURL substring matching). Provider priority is now driven by the caller
via options.providerPriority — an array of provider names in descending
priority order. Providers not listed share the lowest priority. When
omitted, all providers are treated equally and the highest fix version
wins.

Also fixes compareVersions to fall back to lexicographic comparison for
non-numeric version segments.

Implements TC-5410

Assisted-by: Claude Code
- Store provider rank in separate Map instead of _priority field on
  output objects
- Normalize severity to uppercase in higherSeverity() for consistent
  output regardless of provider casing
- Add version comparison in extractFromRecommendations at equal rank
  so recommendations don't silently lose to lower-version source fixes
- Add tests for severity normalization and source+recommendation
  CVE/advisory preservation

Implements TC-5410

Assisted-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants