Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/references/strategies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
- Docker
-->

### bazel

- [bazel (Bzlmod)](languages/bazel/bazel.md)

### clojure

- [leiningen](languages/clojure/leiningen.md)
Expand Down Expand Up @@ -168,6 +172,7 @@ Invoke strict analysis with the `--strict` flag when running `fossa analyze`.
| [C#/.NET (paket)](https://github.com/fossas/fossa-cli/tree/master/docs/references/strategies/languages/dotnet/paket.md) | Static | ❌ |
| [C#/.NET (projectassetsjson)](https://github.com/fossas/fossa-cli/tree/master/docs/references/strategies/languages/dotnet/projectassetsjson.md) | Static | ❌ |
| [C#/.NET (projectjson)](https://github.com/fossas/fossa-cli/tree/master/docs/references/strategies/languages/dotnet/projectjson.md) | Static | ❌ |
| [Bazel (Bzlmod)](languages/bazel/bazel.md) | Dynamic with static fallback | ❌ |
| [C](https://github.com/fossas/fossa-cli/tree/master/docs/references/strategies/languages/c-cpp/c-cpp.md) | Custom | ✅ |
| [C++](https://github.com/fossas/fossa-cli/tree/master/docs/references/strategies/languages/c-cpp/c-cpp.md) | Custom | ✅ |
| [Clojure (leiningen)](https://github.com/fossas/fossa-cli/blob/master/docs/references/strategies/languages/clojure/clojure.md) | Dynamic | ❌ |
Expand Down
62 changes: 62 additions & 0 deletions docs/references/strategies/languages/bazel/bazel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Bazel Analysis

Bazel projects using [Bzlmod](https://bazel.build/external/module) (`MODULE.bazel`) are analyzed for dependencies. This covers `bazel_dep()` entries (modules from the Bazel Central Registry) and Maven artifacts declared via the `rules_jvm_external` extension.

**Requires Bazel 6+** (Bzlmod available). The legacy `WORKSPACE` system is not supported. Bazel 9 (Jan 2026) removed `WORKSPACE` entirely, making `MODULE.bazel` the only dependency management approach.

Dependencies managed by other ecosystems (Go, Node, Cargo, pip, etc.) within a Bazel project are handled by their respective strategies.

| Strategy | Direct Deps | Transitive Deps | Edges | Container Scanning |
| -------------- | ------------------ | ------------------ | ------------------ | ------------------ |
| MODULE.bazel | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |

## Project Discovery

Find a file named `MODULE.bazel`. Subdirectories are not scanned for nested Bazel projects, since Bazel projects are rooted at the `MODULE.bazel` location.

## Analysis

### Static analysis

1. Parse `MODULE.bazel` to extract:
- `bazel_dep(name, version)` entries (reported as `bazel` dependency type)
- `maven.install(artifacts=[...])` from `rules_jvm_external` extensions (reported as `mvn` dependency type)
- Variable references are resolved (e.g. `ARTIFACTS = [...]` used in `maven.install(artifacts = ARTIFACTS)`)
2. If `maven_install.json` exists alongside `MODULE.bazel`, parse it for the full resolved Maven dependency tree with transitive edges.
3. If `maven_install.json` is absent but `maven.install(artifacts=[...])` is present in `MODULE.bazel`, report those artifacts as direct-only dependencies (graph breadth is `Partial`).

### Dynamic analysis

If `bazel` is available on `PATH`, run `bazel mod graph --output json` to obtain the full resolved Bazel module dependency tree with transitive edges.

If `bazel` is not available, analysis falls back to static-only. Use `--static-only-analysis` to skip the dynamic step explicitly.

## Supported files

| File | Required | Purpose |
| --------------------- | -------- | ---------------------------------------------- |
| `MODULE.bazel` | Yes | Bazel module definition and dependency manifest |
| `maven_install.json` | No | Coursier-resolved Maven lockfile |

## FAQ

### How do I only perform analysis for Bazel?

Specify the target in `.fossa.yml`:

```yaml
# .fossa.yml

version: 3
targets:
only:
- type: bazel
```

### What about WORKSPACE-based projects?

Only Bzlmod (`MODULE.bazel`) is supported. Bazel 9 removed `WORKSPACE` support entirely. For older projects, migrate to Bzlmod or use `WORKSPACE.bzlmod` as a transitional step.

### What about Maven artifacts declared in `rules_jvm_external`?

If you use `maven.install(artifacts=[...])` in `MODULE.bazel` and run `bazel run @maven//:pin` to generate `maven_install.json`, the lockfile will be parsed for the full transitive Maven dependency graph. Without the lockfile, only the directly declared artifact coordinates are reported.
7 changes: 7 additions & 0 deletions spectrometer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,10 @@ library
Strategy.AlpineLinux.Types
Strategy.Android.Util
Strategy.ApkDatabase
Strategy.Bazel
Strategy.Bazel.BazelModGraph
Strategy.Bazel.MavenInstall
Strategy.Bazel.ModuleBazel
Strategy.BerkeleyDB
Strategy.BerkeleyDB.Internal
Strategy.Bundler
Expand Down Expand Up @@ -610,6 +614,9 @@ test-suite unit-tests
App.Fossa.VSI.IAT.ResolveSpec
App.Fossa.VSI.TypesSpec
App.Fossa.VSIDepsSpec
Bazel.BazelModGraphSpec
Bazel.MavenInstallSpec
Bazel.ModuleBazelSpec
BerkeleyDB.BerkeleyDBSpec
BundlerSpec
Cargo.CargoTomlSpec
Expand Down
4 changes: 3 additions & 1 deletion src/App/Fossa/Analyze/Discover.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Control.Effect.Reader (Has, Reader)
import Data.Aeson qualified as Aeson
import Discovery.Filters (AllFilters)
import Path (Abs, Dir, Path)
import Strategy.Bazel qualified as Bazel
import Strategy.Bundler qualified as Bundler
import Strategy.Cargo qualified as Cargo
import Strategy.Carthage qualified as Carthage
Expand Down Expand Up @@ -48,7 +49,8 @@ import Types (DiscoveredProject)

discoverFuncs :: DiscoverTaskEffs sig m => [DiscoverFunc m]
discoverFuncs =
[ DiscoverFunc Bundler.discover
[ DiscoverFunc Bazel.discover
, DiscoverFunc Bundler.discover
, DiscoverFunc Cabal.discover
, DiscoverFunc Cargo.discover
, DiscoverFunc Carthage.discover
Expand Down
2 changes: 2 additions & 0 deletions src/DepTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ hydrateDepEnvs = hydrate dependencyEnvironments $ \envs dep ->
data DepType
= -- | An archive upload dependency.
ArchiveType
| -- | Bazel module dependency (BCR)
BazelType
| -- | Bower dependency
BowerType
| -- | A first-party subproject
Expand Down
1 change: 1 addition & 0 deletions src/Srclib/Converter.hs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ verConstraintToRevision = \case
depTypeToFetcher :: DepType -> Text
depTypeToFetcher = \case
ArchiveType -> "archive"
BazelType -> "bazel"
BowerType -> "bower"
CarthageType -> "cart"
CargoType -> "cargo"
Expand Down
115 changes: 115 additions & 0 deletions src/Strategy/Bazel.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
module Strategy.Bazel (
discover,
BazelProject (..),
) where

import App.Fossa.Analyze.Types (AnalyzeProject (analyzeProject, analyzeProjectStaticOnly))
import Control.Effect.Diagnostics (
Diagnostics,
Has,
context,
recover,
)
import Control.Effect.Reader (Reader)
import Data.Aeson (ToJSON)
import Discovery.Filters (AllFilters)
import Discovery.Simple (simpleDiscover)
import Discovery.Walk (
WalkStep (WalkSkipAll),
findFileNamed,
walkWithFilters',
)
import Effect.Exec (AllowErr (Never), Command (..), Exec, execJson)
import Effect.ReadFS (ReadFS, readContentsJson, readContentsParser)
import GHC.Generics (Generic)
import Graphing (Graphing)
import Path (Abs, Dir, File, Path)
import Strategy.Bazel.BazelModGraph (BazelModGraphJson, buildModGraphDeps)
import Strategy.Bazel.MavenInstall (MavenInstallJson, buildMavenInstallGraph)
import Strategy.Bazel.ModuleBazel (buildBazelGraph, moduleBazelParser)
import Types (
Dependency,
DependencyResults (..),
DiscoveredProject (..),
DiscoveredProjectType (BazelProjectType),
GraphBreadth (Complete, Partial),
)

discover :: (Has ReadFS sig m, Has Diagnostics sig m, Has (Reader AllFilters) sig m) => Path Abs Dir -> m [DiscoveredProject BazelProject]
discover = simpleDiscover findProjects mkProject BazelProjectType

findProjects :: (Has ReadFS sig m, Has Diagnostics sig m, Has (Reader AllFilters) sig m) => Path Abs Dir -> m [BazelProject]
findProjects = walkWithFilters' $ \dir _ files -> do
case findFileNamed "MODULE.bazel" files of
Nothing -> pure ([], WalkSkipAll)
Just moduleBazel -> do
let mavenInstall = findFileNamed "maven_install.json" files
project =
BazelProject
{ bazelDir = dir
, bazelModuleFile = moduleBazel
, bazelMavenInstall = mavenInstall
}
pure ([project], WalkSkipAll)

data BazelProject = BazelProject
{ bazelDir :: Path Abs Dir
, bazelModuleFile :: Path Abs File
, bazelMavenInstall :: Maybe (Path Abs File)
}
deriving (Eq, Ord, Show, Generic)

instance ToJSON BazelProject

instance AnalyzeProject BazelProject where
analyzeProject _ project = do
staticResult <- context "Bazel" $ analyzeStatic project
dynamicResult <- context "Bazel dynamic analysis" $ recover (analyzeDynamic (bazelDir project))
case dynamicResult of
Just dynGraph ->
pure $
staticResult
{ dependencyGraph = dependencyGraph staticResult <> dynGraph
, dependencyGraphBreadth = Complete
}
Nothing -> pure staticResult

analyzeProjectStaticOnly _ = context "Bazel" . analyzeStatic

mkProject :: BazelProject -> DiscoveredProject BazelProject
mkProject project =
DiscoveredProject
{ projectType = BazelProjectType
, projectBuildTargets = mempty
, projectPath = bazelDir project
, projectData = project
}

-- | Static analysis: parse MODULE.bazel and optionally maven_install.json.
analyzeStatic :: (Has ReadFS sig m, Has Diagnostics sig m) => BazelProject -> m DependencyResults
analyzeStatic project = do
moduleFile <- context "Parsing MODULE.bazel" $ readContentsParser moduleBazelParser (bazelModuleFile project)
let bazelGraph = buildBazelGraph moduleFile
mavenGraph <- case bazelMavenInstall project of
Just installFile -> do
installJson <- context "Parsing maven_install.json" $ readContentsJson @MavenInstallJson installFile
pure $ buildMavenInstallGraph installJson
Nothing -> pure mempty
let combinedGraph = bazelGraph <> mavenGraph
breadth = case bazelMavenInstall project of
Just _ -> Complete
Nothing -> Partial
manifestFiles = [bazelModuleFile project] <> maybe [] pure (bazelMavenInstall project)
pure
DependencyResults
{ dependencyGraph = combinedGraph
, dependencyGraphBreadth = breadth
, dependencyManifestFiles = manifestFiles
}

-- | Dynamic analysis: run `bazel mod graph --output json` and parse the result.
analyzeDynamic :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m (Graphing Dependency)
analyzeDynamic dir = do
let cmd = Command "bazel" ["mod", "graph", "--output", "json"] Never
modGraph <- execJson @BazelModGraphJson dir cmd
pure $ buildModGraphDeps modGraph
82 changes: 82 additions & 0 deletions src/Strategy/Bazel/BazelModGraph.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module Strategy.Bazel.BazelModGraph (
BazelModGraphJson (..),
BazelModGraphNode (..),
buildModGraphDeps,
) where

import Data.Aeson (
FromJSON (parseJSON),
withObject,
(.:),
(.:?),
)
import Data.Map.Strict qualified as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as Text
import DepTypes (
DepType (BazelType),
Dependency (..),
VerConstraint (CEq),
)
import Graphing (Graphing)
import Graphing qualified

-- | Top-level JSON output from `bazel mod graph --output json`.
data BazelModGraphJson = BazelModGraphJson
{ modGraphKey :: Text
, modGraphVersion :: Text
, modGraphDeps :: [BazelModGraphNode]
}
deriving (Eq, Ord, Show)

-- | A node in the module dependency graph.
data BazelModGraphNode = BazelModGraphNode
{ nodeKey :: Text
, nodeVersion :: Text
, nodeDeps :: [BazelModGraphNode]
}
deriving (Eq, Ord, Show)

instance FromJSON BazelModGraphJson where
parseJSON = withObject "BazelModGraphJson" $ \obj ->
BazelModGraphJson
<$> obj .: "key"
<*> (fromMaybe "" <$> obj .:? "version")
<*> (fromMaybe [] <$> obj .:? "dependencies")

instance FromJSON BazelModGraphNode where
parseJSON = withObject "BazelModGraphNode" $ \obj ->
BazelModGraphNode
<$> obj .: "key"
<*> (fromMaybe "" <$> obj .:? "version")
<*> (fromMaybe [] <$> obj .:? "dependencies")

-- | Build a dependency graph from `bazel mod graph` JSON output.
buildModGraphDeps :: BazelModGraphJson -> Graphing Dependency
buildModGraphDeps root =
-- The root node represents the current module; its deps are direct dependencies.
let directDeps = modGraphDeps root
in mconcat (map (\node -> Graphing.direct (nodeToDep node) <> buildNodeEdges node) directDeps)

buildNodeEdges :: BazelModGraphNode -> Graphing Dependency
buildNodeEdges node =
let parent = nodeToDep node
children = nodeDeps node
directEdges = mconcat [Graphing.edge parent (nodeToDep child) | child <- children]
childEdges = mconcat (map buildNodeEdges children)
in directEdges <> childEdges

nodeToDep :: BazelModGraphNode -> Dependency
nodeToDep node =
Dependency
{ dependencyType = BazelType
, dependencyName = nodeKey node
, dependencyVersion =
if Text.null (nodeVersion node)
then Nothing
else Just (CEq (nodeVersion node))
, dependencyLocations = []
, dependencyEnvironments = mempty
, dependencyTags = Map.empty
}
Loading
Loading