From 561efa549c01e2b01959b6b8b2c341085d1753ff Mon Sep 17 00:00:00 2001 From: Ankur Sinha Date: Mon, 22 Jun 2026 12:20:05 +0530 Subject: [PATCH] feat: add CVE fix skill with dependency analysis script Signed-off-by: Ankur Sinha --- .cursor/skills/fix-cves/SKILL.md | 544 +++++++++++++++++++++++++++++++ scripts/fix-cves/analyze-deps.ts | 353 ++++++++++++++++++++ scripts/fix-cves/tsconfig.json | 11 + 3 files changed, 908 insertions(+) create mode 100644 .cursor/skills/fix-cves/SKILL.md create mode 100644 scripts/fix-cves/analyze-deps.ts create mode 100644 scripts/fix-cves/tsconfig.json diff --git a/.cursor/skills/fix-cves/SKILL.md b/.cursor/skills/fix-cves/SKILL.md new file mode 100644 index 00000000..b1a6dc28 --- /dev/null +++ b/.cursor/skills/fix-cves/SKILL.md @@ -0,0 +1,544 @@ +--- +name: fix-cves +description: >- + Fix CVE vulnerabilities in console-plugin across release branches, or triage + FedRAMP image CVEs against base container images. Use when the user asks to + fix CVEs, resolve security vulnerabilities, patch SRVKP Jira tickets, run + dependency security fixes, or check if OS-level CVEs are fixed in base images. +--- + +# Fix CVEs in Console Plugin + +Batch-fix CVE vulnerabilities across one or more release branches, creating one +PR per branch with full verification evidence. + +## Phase 1: Input + +Three input modes are supported. Modes A and B handle **npm dependency CVEs** (proceed to Phase 2). Mode C handles **FedRAMP image CVEs** (self-contained, does not proceed to Phase 2). + +### Mode A: Paste Jira descriptions (no auth required) + +The user pastes one or more Jira ticket descriptions directly into the chat. +Also expect: + +- **Primary branch** (e.g. `release-v1.15.x`) +- **Additional branches** (optional) + +Parse each pasted description to extract: + +- **SRVKP ticket ID** — look for `SRVKP-\d+` in the text +- **CVE ID** — look for `CVE-\d{4}-\d{4,}` pattern +- **Vulnerable package name** — look for npm package names near keywords like + "package", "component", "dependency", "module", or in a structured field +- **Fixed version** — look for version strings near "fixed in", "patched in", + "upgrade to", ">=", or in a structured field +- **Primary branch** — infer from Jira title suffixes such as [pipelines-1.20] → release-v1.20.x, [pipelines-1.21] → release-v1.21.x + +If the fixed version is not present in the pasted Jira description, inspect any advisory links, CVE references, release notes, attachments, or external URLs included in the pasted content and determine the minimum fixed version from those sources. + +### Mode B: Fetch from Jira + +The user provides one or more SRVKP ticket IDs. + +Use the Jira integration available in the current environment to retrieve the corresponding issue details. + +Extract the following information from each Jira issue: + +- SRVKP ticket ID +- CVE ID +- Vulnerable package name +- Required fixed version +- Primary branch — infer from Jira title suffixes such as [pipelines-1.20] → release-v1.20.x, [pipelines-1.21] → release-v1.21.x +- Reporter account ID +- Reporter display name (used for Jira notifications) + +If the fixed version is not present in the Jira issue description, inspect any referenced advisories, CVE links, attachments, or external resources referenced by the Jira issue and determine the minimum fixed version from those sources. + +### Mode C: FedRAMP image CVE triage + +The user provides one or more CVE IDs (e.g. `CVE-2026-43618, CVE-2026-29518`) for OS-level vulnerabilities reported by FedRAMP against the console-plugin container image. + +These are system package CVEs (rsync, expat, libpng, etc.), not npm dependencies. The goal is to determine whether fixes are available in the base container images and whether a rebuild will resolve them. + +#### C1. Fetch CVE details + +For each CVE, query the Red Hat Security Data API: + +``` +https://access.redhat.com/hydra/rest/securitydata/cve/.json +``` + +Extract: + +- Severity and CVSS score +- Affected package name +- RHEL 9 fix state: check `affected_release` for entries where `product_name` contains "Enterprise Linux 9" +- Fixed package version and advisory ID (if available) +- If no RHEL 9 entry exists in `affected_release`, check `package_state` for the fix state (Affected, Not Affected, Will not fix) + +If the API returns "Not Found", search the web for the CVE to determine if it applies to RHEL/UBI packages or is specific to a different vendor (e.g. Oracle bundled libraries). + +#### C2. Check base images + +Query the Pyxis API to inspect the latest `ubi9/nodejs-24` and `ubi9/nginx-124` images. + +**Step 1 — Get the latest image ID for each repo:** + +``` +https://catalog.redhat.com/api/containers/v1/images?filter=repositories.repository%3D%3Dubi9/nodejs-24%3Barchitecture%3D%3Damd64&sort_by=creation_date%5Bdesc%5D&page_size=1 +``` + +Note the `_id` and `creation_date` from the response. + +**Step 2 — Get the RPM manifest to check installed package versions:** + +``` +https://catalog.redhat.com/api/containers/v1/images/id//rpm-manifest +``` + +For each CVE, search the RPM list for the affected package. Compare the installed version against the fixed version from C1. + +**Step 3 — Check outstanding vulnerabilities:** + +``` +https://catalog.redhat.com/api/containers/v1/images/id//vulnerabilities +``` + +Check if any of the target CVEs appear in the outstanding vulnerabilities list. + +#### C3. Present results + +Present a summary table: + +``` +| CVE | Component | Severity | Fixed Package | nodejs-24 (built ) | nginx-124 (built ) | Status | +|-----|-----------|----------|---------------|--------------------------|--------------------------|--------| +| CVE-2026-43618 | rsync | Important (8.1) | rsync-3.2.5-7.el9_8.2 | Has 3.2.5-7.el9_8 (OLD) | Has 3.2.5-7.el9_8 (OLD) | Rebuild needed | +| CVE-2026-45186 | expat | Important (7.5) | expat-2.5.0-6.el9_8.1 | Has 2.5.0-6.el9_8.1 (FIXED) | Has 2.5.0-6.el9_8.1 (FIXED) | Already fixed | +| CVE-2026-22020 | libpng | Important (7.1) | Not tracked for UBI9 | Not installed | Has system libpng (not affected) | Not applicable | +``` + +Status values: + +- **Already fixed** — the installed version matches or exceeds the fixed version +- **Rebuild needed** — the fix errata is published but the base image hasn't been rebuilt yet; rebuilding the console-plugin image will pick up the fix +- **Awaiting upstream** — the fix errata has not been released yet; no action possible until Red Hat publishes the fix +- **Not applicable** — the CVE does not affect UBI9/RHEL9 system packages (e.g. vendor-bundled libraries), or the package is not installed in the image +- **Not installed** — the affected package is not present in the image + +#### C4. Recommend action + +Based on the results: + +- **If all CVEs are "Already fixed" or "Not applicable"**: report that a rebuild will resolve the FedRAMP findings, no code changes needed. +- **If any CVE is "Rebuild needed"**: the fix RPM is available in the content sets. Rebuilding the container image will pick it up. +- **If any CVE is "Awaiting upstream"**: report that the fix is not yet available and the ticket should remain open until the errata is published. + +Mode C is self-contained. Do not proceed to Phase 2, 3, or 4. + +### After parsing (Modes A and B only) + +If the fixed version cannot be determined from any source, ask the user for clarification before proceeding. + +Collect all CVEs into a tracking table and present it to the user for +confirmation before proceeding: + +``` +| SRVKP | CVE ID | Package | Fixed Version | Status | +|------------|-----------------|-------------|--------------:|--------------------| +| SRVKP-1234 | CVE-2024-12345 | micromatch | 4.0.8 | pending | +``` + +Possible status values: `pending`, `fixed`, `already-remediated`, `triaged`. + +## Phase 2: Branch Setup and Fix Loop + +Process branches one at a time. For each branch: + +### 2a. Checkout, sync, and clean install + +Before switching branches, verify the working tree is clean with `git status --porcelain`. If it returns any output, stop and ask the user before proceeding. + +```bash +git checkout +git pull upstream +rm -rf node_modules yarn.lock +yarn install +``` + +**Critical**: Always remove both `node_modules` and `yarn.lock` and reinstall to regenerate the dependency graph from scratch and avoid stale resolutions. + +### 2b. Analyze each CVE + +For each CVE, run the analysis script: + +```bash +npx ts-node --project scripts/fix-cves/tsconfig.json \ + scripts/fix-cves/analyze-deps.ts \ + --package \ + --fixed-version +``` + +The script outputs JSON containing a `strategy` field. Act on the returned strategy and track the outcome for each CVE: + +| Strategy | Outcome | +| -------------------- | ------------------ | +| `already-remediated` | already-remediated | +| `triage-needed` | triaged | +| `direct-upgrade` | fix-required | +| `parent-upgrade` | fix-required | +| `resolution` | fix-required | + +If all CVEs are marked `already-remediated` or `triaged`, do not create a fix branch or PR for this branch. + +### 2c. Create fix branch + +Only create a fix branch if at least one CVE is marked `fix-required`. + +```bash +git checkout -b fix/cve-batch- +``` + +Proceed with processing each CVE according to the strategy returned by the analysis script. + +### Jira Mentions + +When posting Jira comments in Mode B, notify the reporter using a native Jira mention. Extract the reporter's account ID and display name from the Jira issue, then insert a mention node at the beginning of the comment: + +```json +{"type": "mention", "attrs": {"id": "", "text": "@"}} +``` + +All automated Jira comments (already-remediated, triaged, remediation-completed) should begin with a reporter mention. + +Render the evidence sections (yarn why / npm ls output) as ADF `codeBlock` nodes with language `bash`. + +#### Strategy: `already-remediated` + +The installed version already meets or exceeds the required fixed version. No remediation is required for this CVE on the current branch. + +**If Jira integration is available (Mode B)**, add a comment to the corresponding Jira issue: + +```text + + +[CVE Bot] Automated analysis for on branch : + +Package: +Installed version: +Required fixed version: + +Verification: +The installed version meets or exceeds the required fixed version. + +Result: +This CVE is already remediated on the branch and no code changes are required. + +Evidence: + +$ yarn why + + +$ npm ls --all + +``` + +The evidence comes from the analyze-deps script output (`yarnWhyRaw` field). + +**If using paste mode (Mode A)**, print the comment to the user so they can manually post it on the Jira ticket. + +Mark this CVE as `already-remediated` in the tracking table. + +Skip this CVE — no code changes, no commits, no inclusion in PR. Continue processing the remaining CVEs for the branch. + +#### Strategy: `direct-upgrade` + +The package is a direct dependency (in `dependencies` or `devDependencies`). +Upgrade it: + +```bash +yarn up @ +``` + +Or edit the version in `package.json` directly if `yarn up` does not respect the +exact version, then run `yarn install`. + +#### Strategy: `parent-upgrade` + +The vulnerable package is transitive, but upgrading a direct parent to its +latest version pulls in the fix. The script provides `parentUpgradeSuggestions` +showing which parent to upgrade. For example: + +```json +"parentUpgradeSuggestions": ["eslint@9.0.0 (pulls glob@10.3.0, satisfies 10.3.0)"] +``` + +Upgrade the suggested parent: + +```bash +yarn up @ +``` + +Then verify the transitive dep resolved to the fixed version. + +#### Strategy: `resolution` + +The package is transitive and no parent upgrade resolves it. Add or update the +`resolutions` field in `package.json`: + +```json +"resolutions": { + "": "" +} +``` + +Merge with existing resolutions (currently: `webpack`, `@types/d3-dispatch`, +`@types/d3-selection`). Then run `yarn install`. + +**Resolutions are a last resort.** Only use when neither direct-upgrade nor +parent-upgrade is possible. + +#### Strategy: `triage-needed` + +The fixed version is not published on npm, or the SDK pins an incompatible range. + +**If Jira integration is available (Mode B)**, add a comment to the corresponding Jira issue: + +```text + + +[CVE Bot] Automated analysis for on branch : + +Package: +Installed version: +Requested fixed version: + +Result: +The required fixed version is not currently available on npm, or an SDK dependency constraint prevents the upgrade. + +This package is a transitive dependency of @openshift-console/dynamic-plugin-sdk. + +Evidence: + +$ yarn why + + +$ npm ls --all + + +Please advise on next steps: +- Wait for an upstream dependency update +- Accept a resolution override +- Use an alternative remediation approach +``` + +**If using paste mode (Mode A)**, print the triage comment to the user so they can manually post it on the Jira ticket or forward it to the reporter. + +Mark this CVE as `triaged` in the tracking table and skip it. + +### 2d. Verify fixes + +After all remediation changes have been applied for the branch, verify each fixed package: + +```bash +yarn why +npm ls --all 2>/dev/null || true +``` + +Capture the verification output for each remediated CVE. This evidence should be included in the PR description. + +Verification should confirm that: + +* The vulnerable version is no longer present. +* The required fixed version is installed. +* The dependency tree reflects the expected remediation. + +If verification shows the vulnerable version is still present: + +* Check if another dependency re-introduces it. +* Add an additional resolution if required. +* Re-run the analysis script. +* Re-apply the remediation if necessary. +* Re-verify the package. + +After dependency verification succeeds, validate that the branch still builds and tests successfully: + +```bash +yarn build +yarn test +``` + +Record the result of each command for inclusion in the PR description. + +If a validation step fails: + +* Investigate whether the failure is related to the dependency update. +* Fix the issue if possible. +* Do not mark the CVE as `fixed` until the remediation has been verified and the validation results have been reviewed. +* If build or test failures are unrelated to the remediation, record the results and continue based on user approval. + +After successful verification and validation, update the status of each remediated CVE from `pending` to `fixed`. + +## Phase 3: Create PR + +Only create a PR if at least one CVE resulted in an actual dependency change. If all CVEs for a branch are `already-remediated` or `triaged`, add the appropriate Jira comments, report the branch status to the user, and continue to the next branch. Only include remediated CVEs in commit messages, PR titles, PR descriptions, and Jira comments. + +### 3a. Commit and push + +```bash +git add package.json yarn.lock + +git commit -m "$(cat <<'EOF' +fix(deps): resolve CVEs [, ] on + +Fixes: +- () +- () +EOF +)" + +git push -u origin fix/cve-batch- +``` + +### 3b. Create the PR + +Use `gh pr create` with a descriptive body: + +```bash +gh pr create \ + --base \ + --title "fix(deps): resolve CVEs [, ] on " \ + --body "$(cat <<'EOF' +## CVE Fixes + +| SRVKP | CVE ID | Package | Old Version | New Version | Strategy | +|--------|--------|----------|-------------|-------------|----------| +| | | | | | direct-upgrade | +| | | | | | resolution | + +## Validation + +| Check | Result | +| ---------- | --------------------------- | +| yarn build | | +| yarn test | | + +## Verification Evidence + +### + +
+yarn why + +~~~text + +~~~ + +
+ +### + +
+yarn why / npm ls --all + +~~~text + +~~~ + +
+ +EOF +)" +``` + +### 3c. Update Jira with PR Information + +After creating the PR, perform the following for each remediated CVE that was included in the PR. + +**If Jira integration is available (Mode B):** + +1. **Add a comment** to the corresponding Jira issue: + +```text + + +[CVE Bot] Remediation completed for on branch . + +Package: +Fixed version: +Pull Request: + +Verification: +The vulnerable version is no longer present and the required fixed version has been verified. + +Status: +Remediation completed and PR created for review. +``` + +2. **Add the PR to the Pull Request field.** Use `getJiraIssue` with `fields: ["*all"]` to find the custom field ID for the "Pull Request" or "Pull Requests" field. Then use `editJiraIssue` to set that field with the PR URL. This ensures the PR appears in the dedicated Pull Request field, not just in Web Links. + +3. **Transition the issue to Code Review.** Use `getTransitionsForJiraIssue` to list available transitions, find the transition whose name matches "Code Review" (case-insensitive), then call `transitionJiraIssue` with that transition ID. + +**If using paste mode (Mode A)**, print the comment to the user and instruct them to link the PR and move the ticket to Code Review manually. + +## Phase 4: Repeat for Additional Branches + +For each additional branch the user specified: + +1. Go back to Phase 2 (checkout, sync, clean install, analyze, fix, verify). +2. Create a separate PR for each branch (Phase 3). + +Fixes may differ between branches because dependency trees diverge across releases. Always re-run the dependency analysis for every branch. Do not assume the same fix applies across branches. + +## Shared Packages with OpenShift Console + +The analysis script automatically detects packages shared with the console by +checking if they appear in the transitive dependency tree of: + +- `@openshift-console/dynamic-plugin-sdk` +- `@openshift-console/dynamic-plugin-sdk-internal` +- `@openshift-console/dynamic-plugin-sdk-webpack` + +When a vulnerable package is shared with the SDK: + +1. **Check if the fixed version is available on npm** — the script does this +2. **If available**: use `resolution` to force it (the SDK will pick up the + hoisted version) +3. **If not available**: triage with the Jira reporter (`triage-needed` strategy) +4. **Resolution is the last resort** — only when direct upgrade cannot work + +## Checklist + +Copy this checklist and track progress per branch: + +```text +Branch: release-vX.Y.x + +- [ ] Verified working tree is clean +- [ ] Synchronized branch with upstream +- [ ] Performed clean install +- [ ] Analyzed all CVEs +- [ ] Created fix branch, if remediation was required +- [ ] Applied fixes (direct-upgrade / parent-upgrade / resolution), if required +- [ ] Recorded outcomes for all CVEs (fixed / already-remediated / triaged) +- [ ] Verified all remediated packages with yarn why / npm ls +- [ ] Ran yarn build +- [ ] Ran yarn test +- [ ] Committed and pushed changes, if remediation was required +- [ ] Created PR with evidence, if remediation was required +- [ ] Updated Jira with remediation results +``` + +### Mode C Checklist + +```text +FedRAMP Image CVE Triage + +- [ ] Fetched CVE details from Red Hat Security Data API +- [ ] Queried latest ubi9/nodejs-24 image from Pyxis +- [ ] Queried latest ubi9/nginx-124 image from Pyxis +- [ ] Checked RPM manifests for affected packages +- [ ] Compared installed versions against fixed versions +- [ ] Presented summary table with status per CVE +- [ ] Recommended action (rebuild / wait / not applicable) +``` \ No newline at end of file diff --git a/scripts/fix-cves/analyze-deps.ts b/scripts/fix-cves/analyze-deps.ts new file mode 100644 index 00000000..e21d1dd0 --- /dev/null +++ b/scripts/fix-cves/analyze-deps.ts @@ -0,0 +1,353 @@ +#!/usr/bin/env npx ts-node --project scripts/fix-cves/tsconfig.json + +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import semver from 'semver'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface AnalysisResult { + package: string; + currentVersion: string | null; + fixedVersion: string; + isSharedWithSDK: boolean; + dependencyChains: string[]; + /** Direct parent packages that pull this dep transitively. */ + directParents: string[]; + /** Whether upgrading a direct parent to its latest resolves the CVE. */ + parentUpgradeAvailable: boolean; + parentUpgradeSuggestions: string[]; + fixedVersionAvailable: boolean; + availableVersions: string[]; + strategy: + | 'already-remediated' + | 'direct-upgrade' + | 'parent-upgrade' + | 'resolution' + | 'triage-needed'; + reason: string; + yarnWhyRaw: string; +} + +interface CLIArgs { + package: string; + fixedVersion: string; +} + +// --------------------------------------------------------------------------- +// SDK packages whose transitive deps are "shared with openshift-console" +// --------------------------------------------------------------------------- + +const SDK_PACKAGES = [ + '@openshift-console/dynamic-plugin-sdk', + '@openshift-console/dynamic-plugin-sdk-internal', + '@openshift-console/dynamic-plugin-sdk-webpack', +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function runCmd(cmd: string, args: string[]): string { + try { + return execFileSync(cmd, args, { + encoding: 'utf-8', + maxBuffer: 10 * 1024 * 1024, + }); + } catch (e: any) { + return e.stdout ?? ''; + } +} + +function parseArgs(): CLIArgs { + const args = process.argv.slice(2); + let pkg = ''; + let fixedVersion = ''; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--package' && args[i + 1]) pkg = args[++i]; + else if (args[i] === '--fixed-version' && args[i + 1]) + fixedVersion = args[++i]; + } + if (!pkg || !fixedVersion) { + console.error( + 'Usage: analyze-deps.ts --package --fixed-version ', + ); + process.exit(1); + } + if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(pkg)) { + console.error(`Invalid package name: ${pkg}`); + process.exit(1); + } + if (!semver.valid(fixedVersion)) { + console.error(`Invalid semver version: ${fixedVersion}`); + process.exit(1); + } + return { package: pkg, fixedVersion }; +} + +/** + * Run `yarn why ` and return raw output + parsed dependency chains. + */ +function getYarnWhy(pkg: string): { raw: string; chains: string[] } { + const raw = runCmd('yarn', ['why', pkg]); + const chains: string[] = []; + for (const line of raw.split('\n')) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('=') && !trimmed.startsWith('Done')) { + chains.push(trimmed); + } + } + return { raw, chains }; +} + +/** + * Determine if a package is a transitive dependency of any SDK package + * by inspecting `yarn why` output for SDK package names in the chains. + */ +function isTransitiveSDKDep(chains: string[]): boolean { + return chains.some((chain) => + SDK_PACKAGES.some((sdk) => chain.includes(sdk)), + ); +} + +/** + * Get the currently installed version from node_modules or yarn.lock. + */ +function getCurrentVersion(pkg: string): string | null { + const nmPath = path.join( + process.cwd(), + 'node_modules', + ...pkg.split('/'), + 'package.json', + ); + if (fs.existsSync(nmPath)) { + try { + const pj = JSON.parse(fs.readFileSync(nmPath, 'utf-8')); + return pj.version ?? null; + } catch { + return null; + } + } + return null; +} + +/** + * Get ALL installed versions of a package across the entire node_modules tree. + * Catches nested/duplicate copies that the hoisted-only check would miss. + */ +function getAllInstalledVersions(pkg: string): string[] { + const output = runCmd('npm', ['ls', '--all', pkg, '--json']); + const versions = new Set(); + try { + const tree = JSON.parse(output); + findVersions(tree, pkg, versions); + } catch {} + return [...versions]; +} + +function findVersions( + node: any, + pkg: string, + versions: Set, +): void { + if (!node || typeof node !== 'object') return; + if (node.dependencies) { + for (const [name, dep] of Object.entries(node.dependencies)) { + if (name === pkg && dep.version) versions.add(dep.version); + findVersions(dep, pkg, versions); + } + } +} + +/** + * Fetch available versions from the npm registry. + */ +function getAvailableVersions(pkg: string): string[] { + const output = runCmd('npm', ['view', pkg, 'versions', '--json']); + try { + const parsed = JSON.parse(output); + return Array.isArray(parsed) ? parsed : [parsed]; + } catch { + return []; + } +} + +/** + * Check if this package is a direct dependency or devDependency + * (as opposed to only transitive). + */ +function isDirectDep(pkg: string): boolean { + const pjPath = path.join(process.cwd(), 'package.json'); + if (!fs.existsSync(pjPath)) return false; + const pj = JSON.parse(fs.readFileSync(pjPath, 'utf-8')); + return !!(pj.dependencies?.[pkg] || pj.devDependencies?.[pkg]); +} + +/** + * Parse `yarn why` chains to find the direct (top-level) packages that + * transitively pull in the target package. + */ +function getDirectParents(pkg: string): string[] { + const pjPath = path.join(process.cwd(), 'package.json'); + if (!fs.existsSync(pjPath)) return []; + const pj = JSON.parse(fs.readFileSync(pjPath, 'utf-8')); + const allDirect = new Set([ + ...Object.keys(pj.dependencies ?? {}), + ...Object.keys(pj.devDependencies ?? {}), + ]); + + const output = runCmd('yarn', ['why', pkg]); + const parents = new Set(); + for (const line of output.split('\n')) { + for (const dep of allDirect) { + if (line.includes(dep)) parents.add(dep); + } + } + parents.delete(pkg); + return [...parents]; +} + +/** + * For a transitive dep, check whether upgrading any direct parent to its + * latest version would pull in the fixed version. Returns upgrade suggestions. + */ +function checkParentUpgrades( + pkg: string, + fixedVersion: string, + directParents: string[], +): string[] { + const suggestions: string[] = []; + for (const parent of directParents) { + const latest = runCmd('npm', ['view', parent, 'version']).trim(); + if (!latest) continue; + const depField = runCmd('npm', [ + 'view', + `${parent}@${latest}`, + 'dependencies', + '--json', + ]).trim(); + if (!depField) continue; + try { + const deps = JSON.parse(depField); + const range: string | undefined = deps[pkg]; + if (range && semver.satisfies(fixedVersion, range)) { + suggestions.push( + `${parent}@${latest} (pulls ${pkg}@${range}, satisfies ${fixedVersion})`, + ); + } + } catch { + // skip + } + } + return suggestions; +} + +/** + * Compare two semver strings. Returns true if `installed` >= `required`. + * Falls back to false on any parse error to avoid false negatives. + */ +function isVersionSatisfied(installed: string, required: string): boolean { + try { + return semver.gte(installed, required); + } catch { + return false; + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + const args = parseArgs(); + const { raw: yarnWhyRaw, chains } = getYarnWhy(args.package); + const currentVersion = getCurrentVersion(args.package); + + // Full-tree check: verify ALL installed copies satisfy the fix, not just the + // hoisted one. Falls back to the hoisted version if npm ls returns nothing. + let installedVersions = getAllInstalledVersions(args.package); + if (installedVersions.length === 0 && currentVersion) { + installedVersions = [currentVersion]; + } + const allSatisfied = + installedVersions.length > 0 && + installedVersions.every((v) => isVersionSatisfied(v, args.fixedVersion)); + if (allSatisfied) { + const sharedWithSDK = isTransitiveSDKDep(chains); + const result: AnalysisResult = { + package: args.package, + currentVersion, + fixedVersion: args.fixedVersion, + isSharedWithSDK: sharedWithSDK, + dependencyChains: chains, + directParents: [], + parentUpgradeAvailable: false, + parentUpgradeSuggestions: [], + fixedVersionAvailable: true, + availableVersions: [], + strategy: 'already-remediated', + reason: `All ${installedVersions.length} installed copy/copies satisfy >= ${args.fixedVersion}`, + yarnWhyRaw, + }; + console.log(JSON.stringify(result, null, 2)); + return; + } + + const sharedWithSDK = isTransitiveSDKDep(chains); + const versions = getAvailableVersions(args.package); + const fixedAvailable = versions.includes(args.fixedVersion); + const direct = isDirectDep(args.package); + const directParents = direct ? [] : getDirectParents(args.package); + const parentSuggestions = direct + ? [] + : checkParentUpgrades(args.package, args.fixedVersion, directParents); + + let strategy: AnalysisResult['strategy']; + let reason: string; + + if (!fixedAvailable) { + strategy = 'triage-needed'; + reason = `Fixed version ${args.fixedVersion} not published on npm`; + } else if (direct) { + strategy = 'direct-upgrade'; + reason = sharedWithSDK + ? 'Direct dependency also pulled by SDK; upgrade directly, SDK will use the hoisted version' + : 'Direct dependency — safe to upgrade'; + } else if (parentSuggestions.length > 0) { + strategy = 'parent-upgrade'; + reason = `Upgrading a direct parent pulls in the fix: ${parentSuggestions.join( + '; ', + )}`; + } else if (sharedWithSDK) { + strategy = 'resolution'; + reason = + 'Transitive dep of SDK; no parent upgrade resolves it — use resolutions to force the fixed version'; + } else { + strategy = 'resolution'; + reason = + 'Transitive dependency; no parent upgrade resolves it — use resolutions as last resort'; + } + + const result: AnalysisResult = { + package: args.package, + currentVersion, + fixedVersion: args.fixedVersion, + isSharedWithSDK: sharedWithSDK, + dependencyChains: chains, + directParents, + parentUpgradeAvailable: parentSuggestions.length > 0, + parentUpgradeSuggestions: parentSuggestions, + fixedVersionAvailable: fixedAvailable, + availableVersions: versions.slice(-20), + strategy, + reason, + yarnWhyRaw, + }; + + console.log(JSON.stringify(result, null, 2)); +} + +main(); diff --git a/scripts/fix-cves/tsconfig.json b/scripts/fix-cves/tsconfig.json new file mode 100644 index 00000000..b7f52eb5 --- /dev/null +++ b/scripts/fix-cves/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2020", + "esModuleInterop": true, + "strict": false, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["./*.ts"] +}