Analyzing FOSSA CLI
Source: fossas/fossa-cli @
77a146d
How It Works
FOSSA CLI is a polyglot dependency analysis engine. You point it at a codebase and it automatically discovers which package managers and build systems are present, resolves dependency graphs for each, and uploads the results to the FOSSA SaaS backend for license compliance and vulnerability scanning. It supports ~35 build tools across 25+ languages — from npm and Maven to Cargo, Pub, and RPM — through a plugin-like analyzer architecture.
At a high level, the tool operates in three phases: discover, analyze, and upload. Discovery walks the filesystem looking for manifest files (e.g., package.json, go.mod, pom.xml) and registers projects. Analysis runs the appropriate strategy for each discovered project — parsing lockfiles, shelling out to build tools, or executing custom logic — to produce a Graphing Dependency, which is a directed graph with the notion of “direct” vs. “transitive” dependencies. Finally, the graph is converted to FOSSA’s SourceUnit format and uploaded to the backend API.
Beyond core dependency analysis, FOSSA CLI also runs Ficus (vendored dependency / snippet scanning), Lernie (keyword and custom license search), Millhone (snippet extraction for IP matching), VSI (Vulnerable Systems Integration for OS packages), first-party license scans, and reachability analysis — all orchestrated from the main fossa analyze entrypoint.
Architecture
The codebase is a Haskell + Rust polyglot. The core CLI and all analyzer strategies (~60K lines of Haskell) live in src/. Rust components (~4K lines) live in extlib/ as separate crates within a Cargo workspace. The two languages do not interop via FFI; instead, Rust binaries are embedded as compressed blobs in the Haskell binary via Template Haskell (Data.FileEmbed). At runtime, Haskell extracts them to a temp directory and shells out as subprocesses.
Key Rust crates:
berkeleydb— parses BerkeleyDB format files for RPM package databases on Linux containers. Ported from Go’sgo-rpmdb.millhone— extracts code snippets (C/C++ functions) for IP/attribution matching against FOSSA’s knowledge base.
Key Haskell modules:
src/App/Fossa/— CLI commands (analyze, test, report, container, SBOM, etc.)src/Strategy/— per-ecosystem analyzers (Cargo, Node, Maven, Go, Python, etc.)src/Types.hs/src/DepTypes.hs— core domain types (Dependency,DepType,GraphBreadth)src/Graphing.hs— custom graph data structure wrappingalgaadjacency maps with direct/transitive trackingsrc/Srclib/— conversion layer from internal types to FOSSA’sSourceUnitprotocolsrc/Effect/— effect system (Exec, ReadFS, Logger) using thefused-effectslibrarysrc/Control/Carrier/— effect carriers includingFossaApiClientfor backend communication
The build system uses Cabal (spectrometer.cabal) for Haskell and Cargo for Rust, unified via a Makefile. The cabal package name is spectrometer (a historical artifact).
The Spine
A fossa analyze command flows like this:
- Entry —
app/fossa/Main.hs→App.Fossa.Main.appMainparses CLI opts viaoptparse-applicativesubcommands. - Dispatch —
App.Fossa.Analyze.dispatchreceives anAnalyzeConfig, callsanalyzeMain. - Preflight —
preflightChecksvalidates API credentials and fetches org capabilities from the FOSSA backend. - Manual deps —
analyzeFossaDepsFileparses optional.fossa.yml/fossa-deps.ymlfor user-declared dependencies. - Discovery —
runAnalyzersiterates overdiscoverFuncs, a heterogeneous list of 35+DiscoverFuncvalues. Each callswithDiscoveredProjects, which forks concurrent tasks via aTaskPool. - Per-project analysis — For each discovered project,
runDependencyAnalysisapplies filters, then callsanalyzeProjecton the strategy-specific project type. Each strategy produces aDependencyResultscontaining aGraphing DependencyandGraphBreadth. - Enrichment — Path dependencies are enriched with license data. Ficus runs snippet scans. Lernie runs keyword/license searches. VSI handles OS package analysis.
- Conversion —
Srclib.Converter.projectToSourceUnitconverts eachProjectResultinto aSourceUnitwithLocator-based dependency graphs. - Upload —
uploadSuccessfulAnalysisPOSTs the analysis to the FOSSA API, waits for build completion, and renders a scan summary.
Key Patterns
Strategy pattern via typeclasses. Each ecosystem analyzer implements AnalyzeProject (from App.Fossa.Analyze.Types), which provides analyzeProject and analyzeProjectStaticOnly methods. The DiscoverFunc GADT wraps heterogeneous discover functions into a uniform list, working around Haskell’s lack of impredicative types.
Effect system via fused-effects. The codebase uses an algebraic effect system extensively. Core effects include Exec (shell command execution), ReadFS (filesystem reads), Logger, Diagnostics (error accumulation), FossaApiClient (backend API), and Telemetry. Functions declare effect constraints as typeclass bounds (Has Exec sig m, Has ReadFS sig m).
Embedded binaries. Rust tools and other native binaries are compiled separately, compressed (LZMA for the Themis index), and embedded via Template Haskell. At runtime, extractEmbeddedBinary writes them to $TMP/fossa-vendor-<uuid>/ and cleans up via bracket. This avoids FFI complexity and simplifies cross-compilation.
Graphing with direct/transitive distinction. Graphing wraps algebraic-graphs (alga) adjacency maps, adding a “direct” label to vertices. This is crucial because FOSSA’s backend needs to know which dependencies are direct vs. transitive for license policy evaluation. The shrink/prune operations preserve this distinction.
Configuration layering. CLI options (AnalyzeCliOpts) merge with .fossa.yml config file settings via mergeOpts, with CLI flags taking precedence. This is handled in App.Fossa.Config.Analyze.
Non-Obvious Details
The DiscoverFunc GADT is solving a real Haskell limitation. The discoverFuncs list contains functions returning different project types (NodeProject, CargoProject, etc.). Without impredicative types, you can’t have a list of forall a. ... -> [DiscoveredProject a]. The GADT existential encoding hides the concrete type behind the AnalyzeProject a constraint.
Rust interop is subprocess-based, not FFI. The Strategy.BerkeleyDB.Internal module shows the pattern: Haskell extracts the berkeleydb-plugin binary, base64-encodes the input, pipes it to stdin, and parses JSON from stdout. This is slower than FFI but dramatically simpler for cross-platform builds and avoids Rust/Haskell ABI coupling.
The SourceUnit / Locator protocol is the real interface contract. Srclib.Types and Srclib.Converter define the wire format that the FOSSA backend expects. The Locator type (fetcher$project@revision) is the universal dependency identifier. The depTypeToFetcher function maps ~25 internal DepType variants to backend fetcher strings — and contains several FIXME comments about historical mismatches (e.g., SubprojectType maps to "mvn", GooglesourceType maps to "git").
Ficus is a separate Go-based binary. Despite the Rust extlib pattern, Ficus and Lernie are embedded binaries that are not built from this repo’s Rust code. They appear to be external artifacts downloaded during the build via vendor_download.sh.
spectrometer.cabal is the actual package name. The Haskell package is called spectrometer — likely the original internal name before it became FOSSA CLI. The cabal file is auto-generated; configuration lives in cabal.project and cabal.project.common.
Assessment
Strengths:
- The analyzer architecture is genuinely extensible. Adding a new ecosystem means creating a
Strategy/Foo.hswithdiscoverandAnalyzeProjectinstance, then adding one line todiscoverFuncs. - Effect system usage is disciplined — functions declare exactly which capabilities they need, making the code testable and the dependency graph explicit.
- Concurrent analysis via
TaskPoolwith per-project error isolation (each project’s failures don’t block others) is robust. - The embedded binary pattern sidesteps FFI complexity while keeping the distribution story simple (single binary).
Concerns:
- The
SourceUnitconversion layer (depTypeToFetcher) has accumulated several hacks (threeFIXMEcomments about incorrect mappings). This suggests the backend protocol evolved separately from the CLI and the impedance mismatch is managed with patches rather than refactoring. - The codebase is large for what it does (~60K lines of Haskell). The effect system, while clean, adds significant type machinery overhead. A newcomer must understand fused-effects, alga graphs, and the custom
Graphingwrapper before being productive. - Error handling uses a custom
Diagnosticseffect that accumulates errors and warnings, but the error types are not always well-structured — some strategies usefatalTextwith ad-hoc messages.
Recommendations:
- If extending: start from
Strategy/Cargo.hsorStrategy/Node.hsas reference implementations. They show the full discover → analyze → graph pattern. - If integrating: the
SourceUnitJSON format is the stable contract. Don’t depend on internal types. - The embedded binary approach means Rust tools must communicate via JSON on stdin/stdout. For performance-sensitive operations, this is a bottleneck worth measuring.