STP
SBOM Observer/

Transitive Dependencies

Managing indirect dependencies and deep dependency trees

Software rarely consists of just direct dependencies you explicitly choose. Each dependency brings its own dependencies, which bring theirs, creating multi-level dependency trees. Your application depends on Framework A. Framework A depends on Library B. Library B depends on Utility C. You never chose Utility C, may not know it exists, but your application absolutely depends on it. Vulnerability in Utility C affects your application just as surely as vulnerability in Framework A—but transitive dependencies are invisible in traditional development practices.

Transitive dependencies represent hidden attack surface and operational liability. Organizations that track only direct dependencies miss majority of their component exposure. Modern applications typically have 80-90% of components as transitives rather than directs. Comprehensive SBOM programs must enumerate, track, and manage transitive dependencies with same rigor as direct dependencies. Ignoring transitives creates dangerous blind spots where vulnerabilities hide undetected.

The Transitive Dependency Challenge

Visibility Problem

Direct dependencies are visible in manifest files developers maintain. package.json lists npm dependencies you added. pom.xml documents Maven artifacts you chose. requirements.txt shows pip packages you installed. These manifests don't comprehensively document what those packages depend on—transitive relationships are resolved at build/install time but not explicitly tracked.

Example dependency tree:

my-application
├── express@4.18.2 (direct)
│   ├── accepts@1.3.8 (transitive level 1)
│   │   ├── mime-types@2.1.35 (transitive level 2)
│   │   │   └── mime-db@1.52.0 (transitive level 3)
│   │   └── negotiator@0.6.3 (transitive level 2)
│   ├── array-flatten@1.1.1 (transitive level 1)
│   ├── body-parser@1.20.1 (transitive level 1)
│   │   ├── bytes@3.1.2 (transitive level 2)
│   │   ├── content-type@1.0.5 (transitive level 2)
│   │   └── [...20 more transitive deps]
│   └── [...40 more transitive deps]
├── mongoose@7.0.3 (direct)
│   ├── bson@5.2.0 (transitive level 1)
│   ├── mongodb@5.2.0 (transitive level 1)
│   │   ├── @mongodb-js/saslprep@1.1.0 (transitive level 2)
│   │   ├── bson@5.2.0 (transitive level 2, duplicated)
│   │   └── [...15 more transitive deps]
│   └── [...25 more transitive deps]
└── winston@3.8.2 (direct)
    ├── @colors/colors@1.5.0 (transitive level 1)
    ├── async@3.2.4 (transitive level 1)
    └── [...30 more transitive deps]

Application lists 3 direct dependencies. Actual component count including transitives: 147. Developer explicitly chose Express, Mongoose, Winston. Never consciously selected mime-db, bson, or @mongodb-js/saslprep, but application depends on them. Vulnerability in any of 147 components affects application security.

Responsibility Ambiguity

Who's responsible for transitive dependency security? Component author who included dependency? Root application developer who triggered dependency resolution? Supplier who shipped product? This ambiguity creates gaps where nobody feels accountable.

PSIRT Framework perspective: Root package maintainer responsible for direct dependencies but typically not for transitives. If Library B includes vulnerable Utility C, Library B maintainer should update to patched Utility C and release new Library B version. Root application then updates Library B to get fix. Multi-hop coordination required.

Reality: Transitive dependency updates often lag. Library B maintainer may not prioritize updating Utility C. Root application stuck waiting for Library B update, or must work around by forcing specific Utility C version, or must replace Library B entirely. Remediation complexity increases with dependency depth.

Diamond Dependencies

Single component appearing multiple times in dependency tree at different versions creates "diamond dependency" problem.

my-application
├── package-a@1.0.0
│   └── lodash@4.17.20
└── package-b@2.0.0
    └── lodash@3.10.1

Application transitively depends on lodash twice—version 4.17.20 through package-a, version 3.10.1 through package-b. Depending on dependency resolution strategy (Node.js deduplication, Maven dependency mediation, Python environment), runtime might use one version, both versions, or conflict resolution might fail entirely.

Security implications: If lodash 3.10.1 is vulnerable but 4.17.20 is patched, is application vulnerable? Depends which version actually loads at runtime. SBOM must accurately reflect runtime resolution, not just all possible dependencies.

SBOM Representation of Transitives

Comprehensive SBOMs must enumerate full transitive trees while maintaining understandability.

Flat vs. Hierarchical

Flat representation: All components listed at same level without relationship structure.

{
  "components": [
    {"name": "express", "version": "4.18.2"},
    {"name": "accepts", "version": "1.3.8"},
    {"name": "mime-types", "version": "2.1.35"},
    {"name": "mime-db", "version": "1.52.0"},
    {"name": "negotiator", "version": "0.6.3"}
    // ...142 more components
  ]
}

Advantages: Simple, easy to search ("does SBOM contain component X?"), no duplicate entries.

Disadvantages: Lost dependency relationships, can't answer "why is this component included?", difficult to trace vulnerability impact through dependency chain.

Hierarchical representation: Components structured as dependency tree.

{
  "components": [
    {
      "name": "express",
      "version": "4.18.2",
      "scope": "required",
      "dependencies": [
        {
          "ref": "accepts-1.3.8"
        },
        {
          "ref": "body-parser-1.20.1"
        }
      ]
    },
    {
      "bom-ref": "accepts-1.3.8",
      "name": "accepts",
      "version": "1.3.8",
      "scope": "required",
      "dependencies": [
        {
          "ref": "mime-types-2.1.35"
        }
      ]
    }
  ],
  "dependencies": [
    {
      "ref": "express-4.18.2",
      "dependsOn": ["accepts-1.3.8", "body-parser-1.20.1"]
    },
    {
      "ref": "accepts-1.3.8",
      "dependsOn": ["mime-types-2.1.35", "negotiator-0.6.3"]
    }
  ]
}

Advantages: Preserves relationships, enables impact analysis ("if Library B is vulnerable, which root components are affected?"), supports blame analysis ("why is this component included?").

Disadvantages: More complex, larger file size, requires graph traversal for queries.

Recommended approach: Flat component list for searchability plus separate dependency graph for relationship preservation. CycloneDX supports both simultaneously—flat components array with separate dependencies array.

Depth Indication

Mark component depth in dependency tree for prioritization.

{
  "component": {
    "name": "mime-db",
    "version": "1.52.0",
    "properties": [
      {
        "name": "cdx:dependency:depth",
        "value": "3"
      },
      {
        "name": "cdx:dependency:scope",
        "value": "transitive"
      }
    ]
  }
}

Depth indication helps prioritize remediation. Vulnerability in direct dependency (depth 0) is easier to fix—just update your dependency declaration. Vulnerability in depth-3 transitive requires updating depth-0 dependency, which must update depth-1, which updates depth-2, which finally updates vulnerable depth-3. Coordination overhead increases with depth.

Discovery and Enumeration

Build-Time Enumeration

Most reliable transitive dependency discovery happens during build when dependency resolution occurs.

Language-specific tools:

Node.js/npm:

# npm generates full dependency tree
npm list --all --json > dependency-tree.json

# CycloneDX npm plugin enumerates transitives
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

Maven/Java:

# Maven dependency tree
mvn dependency:tree -DoutputType=json

# CycloneDX Maven plugin
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom

Python/pip:

# pip list with dependencies
pip list --format=json

# CycloneDX Python
cyclonedx-py --output sbom.json

Go modules:

# Go module graph
go mod graph

# CycloneDX Go
cyclonedx-gomod mod -json -output sbom.json

Build-time enumeration captures resolved dependencies—what actually gets included in build artifacts, accounting for version resolution, conflict resolution, and platform-specific variations.

Runtime Analysis

For deployed software where build process is inaccessible, runtime analysis discovers loaded components.

Container image analysis:

# Syft analyzes container layers
syft packages docker:registry/image:tag -o cyclonedx-json

# Grype with SBOM output
grype docker:registry/image:tag -o cyclonedx

Binary analysis: Compiled binaries can be scanned for linked libraries, embedded dependencies, license strings, version identifiers. Less reliable than build-time analysis but useful for black-box scenarios.

Dynamic analysis: Monitor running application, observe which libraries load, create SBOM from runtime behavior. Most accurate for actual execution but misses code paths not exercised during monitoring.

Transitive Vulnerability Management

Impact Analysis

When vulnerability disclosed in transitive dependency, determine impact scope through dependency graph traversal.

def analyze_transitive_vulnerability_impact(sbom, vulnerable_component):
    """Determine which root components are affected by transitive vulnerability"""

    affected_roots = []
    dependency_graph = build_dependency_graph(sbom)

    # Find all components depending on vulnerable component
    depending_components = find_dependents(dependency_graph, vulnerable_component)

    # Trace back to root dependencies
    for component in depending_components:
        roots = trace_to_roots(dependency_graph, component)
        affected_roots.extend(roots)

    # Deduplicate
    affected_roots = list(set(affected_roots))

    return {
        'vulnerable_component': vulnerable_component,
        'direct_dependents': len(depending_components),
        'affected_root_dependencies': affected_roots,
        'impact_scope': categorize_impact(len(affected_roots)),
        'remediation_complexity': estimate_remediation(dependency_graph, vulnerable_component)
    }

Impact analysis answers: "Log4j vulnerability discovered. Which of our direct dependencies need updating to get patched Log4j version?" Dependency graph reveals Express depends on body-parser depends on debug depends on log4j. Must update Express (or wait for Express update) to remediate deep transitive.

Remediation Strategies

Strategy 1: Wait for upstream updates Ideal path. Vulnerable transitive dependency gets patched. Direct dependency updates to patched transitive. You update direct dependency. Clean, maintainable.

Timeline risk: Waiting for multi-hop updates can take weeks. Vulnerable component gets patched day 1. Direct dependency update incorporating patch ships day 7. You deploy updated direct dependency day 10. 10-day exposure window.

Strategy 2: Dependency override/forcing Many package managers support forcing specific transitive versions.

// package.json with npm overrides
{
  "dependencies": {
    "express": "4.18.2"
  },
  "overrides": {
    "mime-db": "1.52.0"  // Force specific mime-db version despite transitive resolution
  }
}
<!-- Maven dependency management -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.15.0</version>  <!-- Override transitive version -->
    </dependency>
  </dependencies>
</dependencyManagement>

Advantages: Immediate remediation without waiting for upstream. You control timing.

Risks: Override might introduce incompatibilities. Direct dependency tested with specific transitive version, you're forcing different version that might break functionality. Regression testing essential.

Strategy 3: Replace direct dependency If direct dependency maintainer is unresponsive to security updates, consider replacing entire dependency with alternative.

Evaluation: Big effort—API compatibility concerns, feature parity verification, significant testing. Only justified for critical vulnerabilities with unresponsive maintainers.

Strategy 4: Vendor patching/forking For critical scenarios, fork vulnerable transitive dependency, apply security patch, use your fork temporarily until official patch propagates through dependency chain.

Maintenance burden: You now maintain fork. Must track upstream, merge future updates, coordinate with direct dependency maintainer to switch back to official version when available.

Communication Patterns

Vulnerability in transitive dependency creates communication challenges across multiple layers.

As software producer: When your product's transitive dependency is vulnerable:

  1. Assess whether vulnerability affects your usage (VEX analysis)
  2. If affected, determine remediation path (update direct dependency, override transitive, replace component)
  3. Publish VEX document explaining status and timeline
  4. Communicate to customers: "We're aware of CVE affecting transitive component X. Status: affected. Fix: Version Y.Z releasing DATE. Workaround: [if available]"

As software consumer: When vendor's product has vulnerable transitive:

  1. Query vendor for VEX status
  2. If vendor unresponsive, assess whether you can force transitive update (depends on access to vendor's dependency configuration)
  3. Evaluate vendor responsiveness—persistent transitive dependency issues indicate poor supply chain hygiene
  4. Consider alternatives if vendor consistently ships vulnerable transitives

Transitive License Compliance

Licenses in transitive dependencies carry same legal obligations as direct dependencies.

License inheritance: Some licenses (GPL) have "viral" or "copyleft" characteristics—including GPL-licensed transitive component may impose GPL obligations on entire application. Organizations with "no GPL" policies must check transitives, not just directs.

License discovery: SBOM license data must cover full transitive tree. Incomplete license documentation for transitives creates compliance blind spots.

def check_transitive_license_compliance(sbom, policy):
    """Verify all transitives comply with license policy"""

    violations = []

    for component in sbom['components']:
        depth = get_component_depth(component)
        licenses = component.get('licenses', [])

        if not licenses:
            violations.append({
                'component': component['name'],
                'depth': depth,
                'issue': 'missing_license_info',
                'severity': 'HIGH' if depth <= 2 else 'MEDIUM'
            })
            continue

        for license in licenses:
            license_id = license.get('license', {}).get('id')
            if license_id in policy['prohibited']:
                violations.append({
                    'component': component['name'],
                    'depth': depth,
                    'license': license_id,
                    'issue': 'prohibited_license',
                    'severity': 'CRITICAL'
                })

    return violations

Remediation: Similar to vulnerability remediation—update direct dependency that brings prohibited transitive, override transitive version with acceptably-licensed alternative, or replace direct dependency entirely.

Managing Transitive Complexity

Dependency Minimization

Fewer dependencies = fewer transitives = reduced attack surface and maintenance burden.

Evaluation criteria: Before adding direct dependency, assess transitive impact:

  • How many transitives does this dependency bring?
  • What licenses do those transitives have?
  • What's the security track record of major transitives?
  • Can simpler alternative achieve same goal with fewer transitives?

Example: Utility library with 2 functions you need brings 47 transitive dependencies. Consider copying those 2 functions (respecting license) rather than importing 47 new components.

Regular Dependency Updates

Keeping direct dependencies current reduces transitive vulnerability accumulation.

Update cadence: Monthly or quarterly dependency update cycles where you refresh direct dependencies to latest stable versions, pulling in transitive updates.

Automation: Tools like Dependabot, Renovate Bot automatically create PRs for dependency updates including transitive refreshes.

Transitive Monitoring

Don't wait for vulnerabilities to check transitives. Continuous monitoring of transitive dependency health, age, security posture.

def monitor_transitive_health():
    """Regular health checks on transitive dependencies"""

    for sbom in get_all_sboms():
        transitives = get_transitive_components(sbom)

        for component in transitives:
            # Check for known vulnerabilities
            vulns = query_vulnerability_databases(component)

            # Check component age
            latest_version = get_latest_version(component)
            if is_significantly_outdated(component['version'], latest_version):
                alert_stale_transitive(component)

            # Check maintenance activity
            repo_activity = get_repository_activity(component)
            if indicates_abandonment(repo_activity):
                alert_abandoned_transitive(component)

Proactive monitoring surfaces transitive risks before they become vulnerabilities or incidents.

Next Steps

On this page