Project 19: Mix Dependency SBOM and License Auditor
Build a reusable Mix task that inventories the resolved dependency graph, emits deterministic CycloneDX-compatible evidence, and enforces explainable source and license policies in CI.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2 - Intermediate/Developer |
| Suggested Seniority | Mid-level |
| Time Estimate | 12-18 hours |
| Main Programming Language | Elixir |
| Alternative Programming Languages | Erlang |
| Coolness Level | Level 3 - Genuinely Clever |
| Business Potential | Level 3 - Service & Support |
| Prerequisites | Mix projects, maps/sets, recursion, JSON basics |
| Key Topics | Mix.Task, dependency graphs, Version, deterministic serialization, CycloneDX, license policy |
1. Learning Objectives
By completing this project, you will:
- Implement a namespaced Mix task with documented arguments, return semantics, and CI-compatible exit behavior.
- Traverse resolved dependency metadata without treating a tree display as a complete graph model.
- Distinguish package identity, requested requirement, locked version, source, checksum, application name, and dependency scope.
- Produce deterministic CycloneDX-compatible component and relationship records.
- Evaluate allow, deny, unknown, and exception policies without pretending license text classification is infallible.
- Test ordinary projects, umbrellas, Git dependencies, path dependencies, optional dependencies, and dependency cycles defensively.
2. All Theory Needed (Per-Concept Breakdown)
Mix Tooling and Reproducible Dependency Evidence
Fundamentals
Mix is Elixir’s build tool, and a Mix.Task is a behaviour-backed command that runs inside the project environment. A task can read project configuration, resolved dependency metadata, the lock file, application information, and command-line options. An SBOM is not simply a list of direct package names: it identifies components and records dependency relationships, versions, sources, checksums, and other evidence. The resolved dependency structure is a graph because multiple parents can share one transitive component. Deterministic output requires stable component identities, sorted properties, sorted relationship lists, one captured timestamp policy, and serialization that does not inherit map enumeration order. License auditing is a policy layer over evidence. Package metadata may contain SPDX-like expressions, free text, multiple licenses, or no declaration, so unknown must remain a first-class result instead of being silently treated as allowed.
Deep Dive into the concept
A good task boundary begins with invocation. Choose a namespaced command such as “mix supply_chain.sbom”. Parse options with strict rules: output path, format, include-development flag, policy file, fail-on severity, project recursion, and reproducible mode. Mix tasks may run once per VM unless re-enabled, which matters in tests that invoke them repeatedly. The task should delegate domain work to ordinary modules so task lifecycle and shell output do not contaminate graph or policy tests.
Dependency evidence has several layers. Project configuration declares dependencies and requirements. Resolution chooses versions and sources. The lock file captures exact Hex package versions/checksums or Git revisions. Runtime/application metadata may expose OTP application names and versions. Do not conflate these. A component record should state what evidence supports each field and should not fabricate a package URL or checksum for a local path dependency.
Model the graph explicitly. Each component receives a stable internal reference. For a Hex package, identity can include ecosystem, normalized package name, and locked version; CycloneDX commonly represents this with a package URL and bom-ref. A Git dependency needs repository and exact revision. A path dependency needs a local identity and a declared nonportable-source marker. Application name may differ from Hex package name, so retain both when available.
Graph traversal needs visited and active sets. A visited set prevents repeated expansion of shared transitives. An active recursion set detects unexpected cycles and turns them into diagnostics instead of infinite recursion. Preserve all edges even when the target component was already visited. Direct versus transitive and environment/scope membership can be calculated from roots and dependency options.
CycloneDX has a versioned schema. Select and document one supported specification version, then validate generated JSON against its official schema during tests. Components and dependencies are separate collections: component metadata identifies nodes, while dependency entries state which bom-ref depends on which other refs. Include tool metadata and project root component. Serial numbers and timestamps can make builds non-reproducible; reproducible mode should omit optional volatile fields or derive them from explicit inputs such as SOURCE_DATE_EPOCH, while normal mode can capture generation time.
License evidence must preserve source and confidence. Hex metadata may declare one or more strings. Normalize exact known SPDX identifiers and expressions only with a documented parser or mapping. Free-form “BSD” is ambiguous; mark it unknown or normalized-with-warning rather than guessing a specific license. A policy can contain allowed SPDX expressions, denied identifiers, denied source types, package-specific exceptions with expiry/reason, and unknown handling.
License expressions are logical, not flat labels. “MIT OR Apache-2.0” differs from “MIT AND Apache-2.0”. A simplistic membership test can incorrectly deny or allow. For baseline scope, either support only single identifiers and explicitly mark expressions unsupported, or use a standards-aware parser. Never split on spaces and claim compliance.
Policy evaluation should produce findings, not immediately halt on the first one. Each finding includes component, evidence, rule id, severity, decision, and remediation. After rendering complete reports, the task maps findings to an exit code. This gives CI a useful artifact even when policy fails. Configuration errors, generation errors, schema validation errors, and policy violations should have distinct exit categories or at least distinct terminal labels.
Umbrella projects complicate roots. The task can produce one aggregate BOM with umbrella root and child applications, or one BOM per child plus aggregate. Pick one baseline. Avoid double-counting dependencies shared by children. Path dependencies inside the umbrella are project components, not external packages. Dev/test-only dependencies require an environment policy; the command must state which Mix environment and targets were evaluated.
Security and trust matter. The tool reads metadata; it should avoid compiling or executing arbitrary dependency code merely to learn licenses. Use Mix’s resolved metadata and lock evidence where possible. Do not fetch the network during a reproducible audit unless explicitly requested. Never include credentials embedded in Git URLs or repository configuration.
The invariants are: every edge references an emitted component; bom-ref values are unique/stable; shared dependencies are emitted once but may have many incoming edges; output ordering is deterministic; source/checksum claims match lock evidence; unknown license remains visible; policy exceptions are scoped and explainable; complete output is written before policy exit; schema version and tool configuration are recorded. Failure modes include tree-only traversal, duplicated components, missing edges, map-order nondeterminism, volatile timestamps, misidentified package/application names, leaked credentials, guessing licenses, mishandled AND/OR expressions, network-dependent reports, and incomplete umbrella traversal.
How this fits in the project
Mix.Task supplies the command integration, graph modules collect/normalize resolved evidence, serializers produce versioned CycloneDX documents, and the policy engine evaluates preserved license/source facts.
Definitions & key terms
- SBOM: Software Bill of Materials describing components and relationships.
- Resolved dependency: Exact source/version selected after requirement resolution.
- Lock evidence: Exact package checksum or Git revision recorded by Mix.
- bom-ref: Stable identifier used to connect CycloneDX nodes and edges.
- SPDX identifier/expression: Standard syntax for declaring software licenses.
- Scope: Context such as runtime, development, test, target, direct, or transitive.
Mental model diagram
mix.exs + resolved deps + mix.lock + app metadata
|
evidence adapters
|
canonical component graph
nodes <--------------> edges
| |
+---- deterministic ---+
normalization
/ \
v v
CycloneDX BOM policy findings
| |
schema check +----> CI exit decision
How it works
- Parse task options and capture project/environment context.
- Read resolved dependencies and lock evidence without running dependency code.
- Normalize package, Git, and path component identities.
- Traverse roots with visited/active sets while preserving every edge.
- Attach license/source/checksum evidence and diagnostics.
- Sort nodes, properties, licenses, and edges.
- Render and schema-validate the BOM.
- Evaluate all policy rules, render findings, then choose exit status.
Minimal concrete example
components:
app@1.0 -> root ref
hex:finch@0.x -> checksum from lock
hex:nimble_pool@1.x -> checksum from lock
relationships:
app ref depends on finch ref
finch ref depends on nimble_pool ref
license policy result:
finch declared Apache-2.0 -> allowed by rule license.allow
local path component has no declaration -> unknown, warning
Common misconceptions
- “mix deps.tree” presentation is not itself a reusable canonical graph.
- The OTP application name and Hex package name need not be identical.
- Missing license metadata does not mean public domain or permissive.
- JSON maps do not communicate required deterministic ordering.
- A report that fails policy can and should still be emitted for diagnosis.
Check-your-understanding questions
- Why preserve edges after a component is already visited?
- Why separate evidence collection from policy evaluation?
- Why is reproducible mode careful about timestamps and random serials?
Check-your-understanding answers
- Shared transitives have multiple parents; skipping edges produces a false graph.
- The same factual BOM can be evaluated under different organizational policies, and unknown evidence must remain visible.
- Volatile values make identical dependency states produce different report bytes.
Real-world applications
Supply-chain review, release evidence, license governance, vulnerability matching, acquisition due diligence, CI policy gates, and regulated deployment inventories rely on SBOMs.
Where you will apply it
Use the concept in task integration, dependency graph construction, component identities, CycloneDX rendering, policy evaluation, umbrella handling, and CI status. It does not overlap P3’s distributed KV, P9’s hot upgrades, or P10’s runtime telemetry; it inspects build metadata.
References
Key insight
A trustworthy SBOM preserves resolved evidence and graph relationships first; policy is an explainable interpretation layered on top.
Summary
The project teaches Mix extension, graph traversal, deterministic data modeling, standards-aware serialization, and careful handling of incomplete supply-chain metadata.
Homework/Exercises
- Draw a diamond graph and list node/edge records.
- Design identities for Hex, Git, and local path dependencies.
- Define policy outcomes for known allowed, known denied, compound expression, missing metadata, and expired exception.
Solutions
- Emit the shared node once but retain both incoming edges.
- Hex uses package/version; Git uses sanitized URL/exact revision; path uses project-relative identity and a local-source marker.
- Preserve allowed/denied/unsupported-or-parsed/unknown/denied-expired with rule evidence.
3. Project Specification
3.1 What You Will Build
Build “mix supply_chain.sbom”, a reusable task that emits a CycloneDX-compatible JSON BOM and a license/source policy report. It supports a normal project and one documented aggregate umbrella strategy.
Scope boundary: Do not implement vulnerability scanning, package installation, signature verification, release upgrading, or a hosted dashboard. Do not execute dependency code to infer metadata.
3.2 Functional Requirements
- Inventory root, direct, and transitive resolved components.
- Support Hex, Git, and path sources without fabricating unavailable evidence.
- Preserve dependency graph edges and relevant environment/target scope.
- Emit deterministic component and dependency ordering.
- Validate against the selected CycloneDX JSON schema.
- Evaluate source/license rules, unknowns, and scoped exceptions.
- Emit reports even when policy produces a failing exit.
3.3 Non-Functional Requirements
- Avoid network access by default.
- Never emit credentials from source URLs/configuration.
- Reproducible mode yields byte-identical output for identical project/lock/policy.
- Diagnostic messages identify evidence source and rule.
- Ordinary domain modules are testable without invoking Mix shell globally.
3.4 Example Usage / Output
$ mix supply_chain.sbom --format cyclonedx-json --output tmp/bom.json \
--policy config/supply-chain-policy.json --reproducible
project=storefront env=prod components=37 relationships=52
sources=hex:34,git:1,path:2 licenses_known=33 unknown=4
findings=warning:3,error:1
ERROR package=legacy_codec rule=license.deny evidence=GPL-3.0-only
bom=tmp/bom.json policy_report=tmp/bom.policy.json
status=policy_failed
3.5 Data Formats / Schemas / Protocols
Canonical graph types include Component, ComponentRef, DependencyEdge, LicenseEvidence, SourceEvidence, Finding, Policy, and RunContext. The BOM declares a fixed CycloneDX schema version. The policy schema contains allowed/denied identifiers, source rules, unknown decision, and exceptions with reason/expiry.
3.6 Edge Cases
- Shared transitives, duplicate app/package names, overridden dependencies.
- Git branch requested but exact revision locked.
- Missing lock entry, local path outside project, umbrella child dependency.
- No license, free-text license, multiple licenses, AND/OR expression.
- Credentials in Git URLs, cycles/inconsistent metadata, unavailable schema.
- Task invoked twice in the same test VM.
3.7 Real World Outcome
3.7.1 How to Run
Run against small fixture Mix projects representing Hex, Git, path, shared-transitive, and umbrella shapes. Validate BOM files with the pinned official schema and compare reproducible checksums.
3.7.2 Golden Path Demo
A fixture diamond graph emits four components and four relationships, not a duplicated shared node. A denied license fails policy after writing both BOM and findings.
3.7.3 Exact Terminal Transcript
$ mix supply_chain.sbom --fixture test/fixtures/diamond --output tmp/a.json --reproducible
components=4 relationships=4 findings=0 status=ok
$ mix supply_chain.sbom --fixture test/fixtures/diamond --output tmp/b.json --reproducible
components=4 relationships=4 findings=0 status=ok
$ shasum -a 256 tmp/a.json tmp/b.json
9ca2... tmp/a.json
9ca2... tmp/b.json
4. Solution Architecture
4.1 High-Level Design
Mix task -> run context -> metadata adapters -> graph builder
|
canonical evidence graph
/ \
CycloneDX renderer policy engine
| |
schema validator findings renderer
\ /
final exit policy
4.2 Key Components
- Mix task and strict option parser
- Project/dependency/lock evidence adapters
- Component identity and source sanitization
- Cycle-safe graph builder
- CycloneDX mapper/serializer/schema validator
- License normalization and policy engine
- Terminal/JSON findings renderers
4.3 Data Structures
Use maps keyed by stable bom-ref for components, MapSet or lists for unique edges, ordered evidence/finding lists, and a RunContext with project, Mix environment/target, reproducibility inputs, and configuration.
4.4 Algorithm Overview
Collect roots and evidence, traverse dependencies preserving edges, validate graph closure/identity uniqueness, enrich licenses, sanitize sources, sort, render BOM, validate schema, evaluate all policy rules, write reports atomically, then set status.
5. Implementation Guide
5.1 Development Environment Setup
Use supported Elixir/OTP and Mix. Pin a CycloneDX schema fixture/version for offline tests. Create tiny fixture projects without real credentials or untrusted dependency execution.
5.2 Project Structure
lib/mix/tasks/supply_chain.sbom
lib/supply_chain/
context
evidence
graph
component_ref
cyclonedx
policy
report
test/fixtures/projects/
5.3 The Core Question You’re Answering
How can a Mix extension turn resolved build metadata into reproducible, standards-shaped evidence and enforce policy without obscuring uncertainty?
5.4 Concepts You Must Understand First
Understand Mix task lifecycle, Mix project/dependency metadata, lock semantics, graph traversal, Version/requirements, deterministic sorting, JSON schema validation, SPDX expression boundaries, and shell exit semantics.
5.5 Questions to Guide Your Design
- What is the stable identity for each source type?
- Which evidence is requested versus resolved?
- How are shared nodes and all edges preserved?
- What makes reproducible output byte-stable?
- What is unknown, and may policy fail on it?
- How are exceptions reviewed and expired?
5.6 Thinking Exercise
Draw root A depending on B and C, both depending on D. Add D under a different OTP application name than package name. Show requested constraints, locked versions, component refs, and four graph edges.
5.7 The Interview Questions They’ll Ask
- How do you create and test a custom Mix task?
- Why is the dependency model a graph rather than a tree?
- What information comes from mix.exs versus mix.lock?
- How do you make JSON generation reproducible?
- Why should unknown license metadata remain explicit?
- How should a CI task behave when policy fails after report generation?
5.8 Hints in Layers
Hint 1: Make one fixture project emit canonical components before CycloneDX.
Hint 2: Separate component discovery from edge preservation.
Hint 3: Build deterministic bom-ref values and assert graph closure.
Hint 4: Render/validate the BOM before evaluating policy and deciding process status.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Mix project organization | “Programming Elixir >= 1.6” by Dave Thomas | Ch. 13, Organizing a Project |
| Software supply chains | “Building Secure and Reliable Systems” by Google | Ch. 14, Configuration Design; Ch. 18, Recovery and Aftercare |
| Evolution and compatibility | “Building Evolutionary Architectures, 2nd Edition” | Ch. 3, Engineering Incremental Change; Ch. 4, Architectural Coupling |
5.10 Implementation Phases
Phase 1: Foundation (4-5 hours)
Implement task shell, run context, fixture projects, evidence types, source sanitization, and direct dependency inventory.
Phase 2: Core Functionality (4-7 hours)
Add transitive graph traversal, stable refs, CycloneDX mapping, deterministic serialization, and schema validation.
Phase 3: Polish & Edge Cases (4-6 hours)
Add licenses/policy/exceptions, umbrella mode, Git/path fixtures, reproducibility, atomic output, and task re-enable tests.
5.11 Key Implementation Decisions
Document CycloneDX version, root/umbrella model, source identities, scope/environment handling, offline behavior, timestamp/serial reproducibility, license-expression support, unknown policy, exceptions, and exit codes.
6. Testing Strategy
6.1 Test Categories
- Strict CLI/task lifecycle tests
- Evidence adapter tests
- Graph closure/shared-node/cycle tests
- CycloneDX schema and deterministic snapshot tests
- Source credential-redaction tests
- License policy decision-table tests
- Fixture project/umbrella end-to-end tests
6.2 Critical Test Cases
- Diamond graph emits shared component once and every edge.
- Git requested branch and locked revision remain distinct.
- Local path source does not receive a fake checksum/package URL.
- Unknown license follows configured policy.
- Compound unsupported expression is not guessed.
- Policy failure still leaves valid BOM/report files.
- Reproducible runs are byte-identical.
- Source credentials never appear in output.
6.3 Test Data
Create miniature Mix fixtures for direct/transitive/diamond, Git, path, missing metadata, umbrella, optional/dev-only, and sanitized credential-looking URLs. Pin BOM expected data independently of runtime map order.
6.4 Definition of Done
- A custom Mix task documents options and exit behavior.
- Component identities and all graph edges are stable and complete.
- Hex, Git, and path evidence are represented honestly.
- Output validates against a pinned CycloneDX schema.
- Reproducible mode produces byte-identical BOMs.
- Unknown licenses and exceptions remain explicit.
- Policy failures emit complete artifacts before nonzero exit.
- Tests cover ordinary and umbrella fixtures.
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
Missing relationships: Traversal stopped when a node was visited. Skip expansion but retain the edge.
Duplicate components: Identity used application name in one path and package name in another. Centralize refs.
Nondeterministic BOM: Maps, timestamps, or random serials leaked into output. Normalize and sort all collections.
License false confidence: Free text was mapped aggressively. Preserve raw evidence and unknown status.
7.2 Debugging Strategies
Render a canonical graph table before CycloneDX mapping. For each field, expose evidence origin in debug mode. Validate graph closure and unique refs separately from JSON schema. Compare normalized documents when hashes differ.
7.3 Performance Traps
Repeated dependency expansion, repeated lock parsing, network metadata fetches, and quadratic list membership checks waste work. Index evidence once and use MapSet for traversal state.
8. Extensions & Challenges
8.1 Beginner Extensions
Add Markdown summaries, dependency depth, direct/transitive filters, and license counts.
8.2 Intermediate Extensions
Support additional CycloneDX formats, package-specific policy waivers, baseline diffing, and SARIF-like CI findings.
8.3 Advanced Extensions
Add signed attestations, release artifact correlation, vulnerability database adapters as a separate stage, and a publishable Hex package with compatibility tests.
9. Real-World Connections
9.1 Industry Applications
Organizations generate SBOMs for procurement, release governance, vulnerability response, customer evidence, licensing, and regulated deployments.
9.2 Related Open Source Projects
CycloneDX and SPDX provide standards context; Hex/Mix provide resolved ecosystem evidence. Study established SBOM tools for interoperability while keeping collection and policy boundaries explicit.
9.3 Interview Relevance
The project demonstrates Mix internals, graph algorithms, deterministic builds, standards mapping, security boundaries, CI ergonomics, and honest handling of incomplete metadata.