Skip to content

Detect license changes in diff (parsed Component.license is currently extracted but never compared) #9

@dmchaledev

Description

@dmchaledev

Summary

The parser already extracts a license for every component, stores it on Component.license, and then diff() silently discards it. For a package whose keywords include supply-chain-security and vulnerability-management, a dependency quietly changing its license (e.g. MIT → AGPL‑3.0, or any permissive → copyleft transition) is a material legal/compliance risk — and the tool already has all the data needed to surface it, but doesn't.

This proposes adding license-change detection to the diff output. It's a natural, low-risk extension of the existing model that turns already-parsed-but-unused data into a real signal.

Evidence (current state)

The data is parsed on both code paths but never used downstream:

  • src/parser.ts — CycloneDX: extractCycloneDXLicense(c) populates Component.license (parser.ts:33, parser.ts:106).
  • src/parser.ts — SPDX: licenseConcluded populates Component.license (parser.ts:66).
  • src/types.tsComponent.license?: string is defined (types.ts:19) but ChangeReport has no license field (types.ts:70).
  • src/diff.tsdiff() compares version for upgrades but never reads .license (diff.ts:23).
  • src/reporter.ts — no license column/section in any of text / json / markdown.

So a component that stays at the same version but changes its declared license produces zero output today.

Proposed change

When a component exists in both SBOMs (matched by the same key diff() already uses) and its license differs, record it as a license change.

1. Types (src/types.ts)

export interface LicenseChange {
  component: Component;
  from?: string;   // license in the old SBOM (undefined if previously unset)
  to?: string;     // license in the new SBOM
}

export interface ChangeReport {
  // ...existing fields...
  licenseChanges: LicenseChange[];
  summary: {
    // ...existing counts...
    totalLicenseChanges: number;
  };
}

2. Diff (src/diff.ts)

In the existing matched-component branch (the same place upgrades are detected), add:

if ((aComp.license ?? '') !== (bComp.license ?? '')) {
  licenseChanges.push({ component: bComp, from: aComp.license, to: bComp.license });
}

This reuses the same component map / matching logic, so it composes with whatever key strategy diff() settles on (note PR #8 is reworking the purl key — this slots in cleanly either way).

3. Reporter (src/reporter.ts)

Add a "License Changes" section to text and markdown (mirroring the existing Upgraded section). json is automatic since it serializes the full report. Example markdown:

## ⚖️ License Changes

| Component | From | To |
|-----------|------|----|
| left-pad  | MIT  | AGPL-3.0 |

4. Tests (src/__tests__/diff.test.ts)

  • same version, license MIT → AGPL-3.0 ⇒ one licenseChanges entry
  • license unchanged ⇒ none
  • license newly added (undefined → MIT) ⇒ one entry (or document that this is intentionally reported)

Why this is high-leverage

  • Aligned with the stated purpose: the README targets "security engineers, DevSecOps teams, and supply-chain risk analysts … for compliance evidence." License drift is squarely in that remit.
  • Zero new parsing / dependencies: the field is already extracted on both formats; this only wires existing data into the diff + report.
  • Composable with the CI gate (PR feat(cli): add --fail-on CI/CD gate, --help/--version, robust arg parsing #5): a future --fail-on license-change becomes trivial once licenseChanges exists in the report.
  • Backward compatible: purely additive to ChangeReport; no change to existing fields or default CLI behavior.

Happy to open a focused PR for this once the in-flight diff/CLI PRs (#5#8) land, to avoid conflicts in diff.ts / reporter.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions