Metadata-Version: 2.4
Name: sbom-git-sm
Version: 0.3.0
Summary: A tool to create a Software Bill of Materials (SBOM) from a git repository based on its submodules
Author-email: Janosch Meyer <janosch.code@proton.me>
License: MIT License
        
        Copyright (c) 2025-2026 Janosch Meyer (janosch.code@proton.me)
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
        Note: This project was created with the assistance of artificial intelligence.
Keywords: sbom,git,submodules,software bill of materials,dependencies
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Software Distribution
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: yaml
Requires-Dist: PyYAML; extra == "yaml"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: coverage[toml]; extra == "dev"
Dynamic: license-file

﻿# sbom-git-sm

A tool to create a Software Bill of Materials (SBOM) from a git repository based on its submodules.

## Features

- Analyzes Git repositories and their submodules recursively
- Collects information about each repository:
  - Commit hash
  - Current branch
  - Tags pointing to the current commit
  - Remote URL of the repository
- Maintains the hierarchical structure of submodules in the output
- Outputs results in CycloneDX JSON format
- Command-line interface for easy integration into scripts and CI/CD pipelines
- Python API for programmatic usage
- Cross-platform compatibility (Windows and Linux)

## Installation

Make sure you have Python 3.7+ installed, then install the package:

```bash
pip install sbom-git-sm
```

## Usage

### Using the Command-line Interface

After installing the package:

```bash
sbom-git-sm [repo_path] [--output output_path] [--pretty]
```

### Using the Module Directly

If you haven't installed the package, you can run it as a module:

```bash
# Windows
python -m sbom_git_sm [repo_path] [--output output_path] [--pretty]

# Linux
python3 -m sbom_git_sm [repo_path] [--output output_path] [--pretty]
```

### Running the Script Directly

You can also run the cli.py script directly:

```bash
# Windows
python sbom_git_sm\cli.py [repo_path] [--output output_path] [--pretty]

# Linux
python3 sbom_git_sm/cli.py [repo_path] [--output output_path] [--pretty]
```

### Arguments

- `repo_path`: Path to the git repository (optional, defaults to current directory)
- `--output`, `-o`: Path to save the SBOM to (optional, if not provided, the SBOM will be printed to stdout)
- `--version`, `-v`: Show version information and exit
- `--pretty`, `-p`: Pretty-print the JSON output
- `--format`: Output format for the SBOM (currently only 'cyclonedx' is supported)
- `--spec-version`: CycloneDX specification version (currently only 1.4 is supported)
- `--component-type`: Override the default component type (default: "application" for main repo, "library" for submodules)
- `--nested-components`, `-n`: Use nested components instead of dependencies structure for representing hierarchical relationships
- `--no-dedup-submodules`: Keep every checkout of a submodule as a separate, `?path=`-qualified component instead of merging same-name-and-commit checkouts into one (see [Submodule Deduplication](#submodule-deduplication)). Ignored with `--nested-components`.
- `--version-config`, `-c`: Path to version configuration file (JSON or YAML) for custom version extraction

### Example

```bash
# Generate a CycloneDX SBOM for the current directory and save it to sbom.json
sbom-git-sm --output sbom.json --pretty

# Generate a CycloneDX SBOM for a specific repository
sbom-git-sm C:\path\to\git\repository --output repo_sbom.json --pretty

# Generate a CycloneDX SBOM with custom version extraction
sbom-git-sm --output sbom.json --version-config version_config.json --pretty
```

## Version Extraction

The version of each repository and submodule is determined in this priority order:

1. A version extracted via a version configuration file (see below)
2. A git tag pointing exactly at the checked-out commit (`version:source: tag`)
   -- tags must be present in the checkout, so shallow clones may need
   `git fetch --tags`
3. The short git commit hash (first 8 characters) as fallback

#### Tag Selection

If several tags point at the same commit, the tool picks the one that
describes the commit best, deterministically and without configuration:

1. Version-like tags win. A tag is version-like when it ends in a dotted
   number with at least two numeric groups, optionally preceded by a
   namespace/`v` prefix and followed by a pre-release suffix
   (`release-v1.1.1`, `Release/0.1.0`, `2026.08`, `1.2.3.4`). Moving pointer
   tags such as `latest-v1.X`, `stable`, or `nightly` are therefore never used
   as a version.
2. Among version-like tags the highest version wins: the numeric part is
   compared element-wise, a release outranks any pre-release of the same number
   (`0.1.0-a.20` < `0.1.0`), and pre-release suffixes are compared segment by
   segment with numeric segments ordered numerically (`rc.2` < `rc.10`) and
   text segments case-insensitively (`alfa`/`alpha` < `beta` < `rc`).
   Abbreviated keywords and mixtures of both spellings order the same way,
   because the comparison is decided by the leading letters:
   `a` < `b` < `rc`, `alpha` < `b`, `a.20` < `beta`, `A.20` < `B.1`.

   This works for keywords whose alphabetical order matches their maturity,
   which covers the classic `alpha`/`beta`/`rc` ladder in both spellings. It
   does not hold for keywords outside that ladder -- `dev` would outrank
   `alpha`, `milestone` would outrank `beta`, and `snapshot` would outrank
   `rc`. Such tags only matter when several *differently mature* pre-release
   tags sit on the *same* commit; repositories that do this can pick the
   intended tags explicitly with `tag_pattern`.
3. If no tag is version-like, a single tag is still used as the version. Among
   several, an annotated tag beats a lightweight one, ties are broken
   lexicographically.

All tags pointing at the commit remain available as repeated `git:tag`
properties regardless of which one becomes the version. Repositories with their
own tag conventions can override the selection per repository with
`tag_pattern` (see [Configuration Fields](#configuration-fields)).

Tag versions are normalized by default: for a version-like tag the numeric
part is used (`release-v1.1.1` yields `1.1.1`), and otherwise a namespace
prefix up to the last `/` and a leading `v`/`V` directly followed by a digit
are stripped, so the tags `Release/v1.0.1` and `v1.0.1` both yield the
component version `1.0.1`. The original tag name is always preserved in the
`git:tag` property. Pass `--raw-tag-version` to use tags verbatim instead.

### Version Configuration File

The version configuration file allows you to specify regex patterns to extract versions from files in the repository and its submodules. The configuration file can be in either JSON or YAML format.

Example configuration files are available in the `examples` directory:
- [version_config.json](examples/version_config.json)
- [version_config.yaml](examples/version_config.yaml)

#### JSON Format Example

```json
{
  "main": {
    "file_pattern": "version.txt",
    "regex_pattern": "version\\s*=\\s*['\"]([^'\"]+)['\"]",
    "encoding": "utf-8"
  },
  "submodules": [
    {
      "name_pattern": "SubmoduleName",
      "file_pattern": "version.txt",
      "regex_pattern": "version\\s*=\\s*['\"]([^'\"]+)['\"]",
      "encoding": "utf-8"
    },
    {
      "name_pattern": ".*",
      "file_pattern": "*.txt",
      "regex_pattern": "([0-9]+\\.[0-9]+\\.[0-9]+)"
    }
  ]
}
```

#### YAML Format Example

```yaml
main:
  file_pattern: version.txt
  regex_pattern: version\s*=\s*['"]([^'"]+)['"]
  encoding: utf-8

submodules:
  - name_pattern: SubmoduleName
    file_pattern: version.txt
    regex_pattern: version\s*=\s*['"]([^'"]+)['"]
    encoding: utf-8
  - name_pattern: .*
    file_pattern: "*.txt"
    regex_pattern: ([0-9]+\.[0-9]+\.[0-9]+)
```

#### Special Characters in YAML Regex Patterns

When using YAML configuration files, you need to be careful with regex patterns that contain special characters, especially the `#` character. In YAML, the `#` character is used to denote comments, so any text after a `#` is ignored unless the text is properly quoted.

For example, if you want to match a C/C++ style define statement like `#define VERSION "1.0.0"`, you need to enclose the regex pattern in quotes:

**INCORRECT - This will not work because # starts a comment in YAML:**
```yaml
submodules:
  - name_pattern: TestSub1
    file_pattern: Version.h
    regex_pattern: #define\s+VERSION\s+"([0-9]+\.[0-9]+\.[0-9]+)"
    # ^ Everything after the # is treated as a comment!
```

**CORRECT - Enclose the pattern in quotes to include the # character:**
```yaml
submodules:
  - name_pattern: TestSub1
    file_pattern: Version.h
    regex_pattern: "#define\\s+VERSION\\s+\"([0-9]+\\.[0-9]+\\.[0-9]+)\""
```

Other special characters that might need special handling in YAML include:
- Colons `:` (must be followed by a space when used as a key-value separator)
- Quotes `"` and `'` (need to be escaped or enclosed in the other type of quotes)
- Backslashes `\` (need to be doubled when inside double quotes)

When in doubt, always enclose your regex patterns in quotes in YAML files.

### Configuration Fields

- `main`: Configuration for the main repository
  - `file_pattern`: Glob pattern to match files to search for version information
  - `regex_pattern`: Regular expression to extract version from file content (first capture group is used)
  - `tag_pattern`: Optional regular expression that replaces the default tag
    selection for this repository (first capture group is the version)
  - `encoding`: Optional text encoding to use when reading files (default: utf-8, with BOM detection fallback)

- `submodules`: Array of configurations for submodules
  - `name_pattern`: Regular expression to match submodule names
  - `file_pattern`: Glob pattern to match files to search for version information
  - `regex_pattern`: Regular expression to extract version from file content (first capture group is used)
  - `tag_pattern`: Optional regular expression that replaces the default tag
    selection for this repository (first capture group is the version)
  - `encoding`: Optional text encoding to use when reading files (default: utf-8, with BOM detection fallback)

#### Tag Patterns

`tag_pattern` steers the [tag selection](#tag-selection) for repositories with
their own tag conventions -- build numbers, date-based schemes, or namespaces
the default heuristic deliberately ignores. It works like `regex_pattern` does
for files: only matching tags are candidates, and capturing group 1 is used as
the version, verbatim.

```yaml
submodules:
  - name_pattern: "legacy-firmware"
    tag_pattern: "^build-(\\d+)$"                     # build-4711 -> 4711
  - name_pattern: "internal-.*"
    tag_pattern: "^Release/(\\d{4}-\\d{2}-\\d{2})$"   # CalVer with dashes
  - name_pattern: "some-lib"
    file_pattern: "version.txt"                        # combinable with file parsing
    tag_pattern: "^v(\\d+\\.\\d+\\.\\d+)$"
```

### Behavior

1. When a configuration is provided, the tool will attempt to extract versions using the specified patterns.
2. If a version cannot be extracted but a configuration exists, the tool will fall back to using the git commit hash and issue a warning.
3. If no configuration exists for a repository or submodule, the git commit hash will be used without a warning.
4. For submodules, the first matching configuration (based on `name_pattern`) will
   be used. Matching remains based on the local checkout folder name, not the
   component name resolved for the SBOM.
5. If multiple files match and contain a valid version, the tool uses the first match in deterministic path order and emits a warning listing all matches.
6. `tag_pattern` only replaces the tag *selection*; the priority chain
   configuration file > tag > hash is unchanged, so an entry may combine
   `file_pattern`/`regex_pattern` with `tag_pattern`.
7. If a `tag_pattern` is configured but no tag pointing at the commit matches
   it, the tool falls back to the commit hash and issues a warning instead of
   guessing a version from a non-matching tag.

### Version Source Property

A property `version:source` is added to each component indicating whether the version was extracted from a file (`file`), taken from a git tag pointing at the commit (`tag`), or derived from the git commit hash (`hash`).

### YAML Support

YAML configuration files require the PyYAML package. You can install it in two ways:

1. Install the package with YAML support using the optional dependency:

```bash
pip install sbom-git-sm[yaml]
```

2. Or install PyYAML separately if you've already installed the package:

```bash
pip install PyYAML
```

## CycloneDX Output

The tool generates a CycloneDX Software Bill of Materials (SBOM) in JSON format. CycloneDX is a lightweight SBOM standard designed for use in application security contexts and supply chain component analysis.

Each Git repository and submodule is represented as a component in the CycloneDX document with the following information:

- Component type: main repository = "application", submodules = "library" (overridable for the main repo via `--component-type`)
- Component name: For submodules, resolved from `.gitmodules` and repository
  metadata as described below; the main repository remains path-based
- Component version: Configured version, normalized exact tag, or short commit
  hash (see [Version Extraction](#version-extraction))
- Package URL (purl): Constructed from the normalized remote URL (see
  [Package URL Scheme](#package-url-scheme)); the purl version is the
  component version for configured/tag versions and the full commit hash
  otherwise
- Properties:
  - git:branch: The current branch
  - git:commit: The full commit hash
  - git:commit.short: The short commit hash
  - git:path: The repository path (relative to the root repository for submodules)
  - git:worktree.path: The repository path (absolute in the main repo, relative for submodules)
  - git:tag: Any tags pointing to the current commit (if available)
  - git:submodule.name: The submodule section name from `.gitmodules`
  - git:submodule.path: The checkout path declared in `.gitmodules`
  - git:submodule.url: The sanitized URL declared in `.gitmodules`
  - git:origin.url: The sanitized local origin URL when it differs from the
    declared submodule URL
  - sbom-git-sm:name.source: The source used to resolve the component name
  - sbom-git-sm:occurrence.count: Number of merged checkouts, only present on
    deduplicated components (see [Submodule Deduplication](#submodule-deduplication))
- External references: The repository URL (if available)

### Package URL Scheme

The purl type and coordinates are derived from the component's remote URL, so
the same repository gets the same identity regardless of the transport used to
clone it (HTTPS, SSH, SCP-style, with or without `.git`, credentials, or port):

| Remote host | purl form |
|---|---|
| github.com | `pkg:github/<owner>/<repo>@<version>` (lowercase coordinates) |
| bitbucket.org | `pkg:bitbucket/<owner>/<repo>@<version>` (lowercase coordinates) |
| Azure DevOps (`dev.azure.com`, `ssh.dev.azure.com`, `<org>.visualstudio.com`) | `pkg:git/dev.azure.com/<org>/<project>/_git/<repo>@<version>` |
| Any other host (GitLab, self-hosted, ...) | `pkg:git/<host>/<path>@<version>` |
| No usable remote URL | `pkg:git/<name>@<version>` (legacy fallback) |

`<version>` is the component version when it comes from a configuration file
or an exact tag (see [Version Extraction](#version-extraction)), and the
**full** commit hash when the version falls back to the hash.

This follows the registered purl types `github`, `bitbucket`, and `git` (host
namespace required). The host/owner namespace keeps components with generic
repository names (`core`, `utils`, ...) distinguishable in SBOM consumers such
as OWASP Dependency-Track, which identify components portfolio-wide by purl.

### Submodule Name Resolution

Submodule names do not depend on the local checkout folder. They are resolved
once during repository analysis in this order:

1. Repository name from the sanitized `.gitmodules` URL (`gitmodules_url`)
2. Section name from `.gitmodules` (`gitmodules_name`)
3. Repository name from the checked-out submodule's `remote.origin.url` (`origin_url`)
4. Last checkout-path segment (`path`)

The last repository segment is supported for GitHub, GitLab, Azure DevOps,
self-hosted HTTPS/SSH and SCP-style URLs, relative URLs, and local POSIX or
Windows paths. A trailing `.git` is removed. No additional name configuration
is required.

For example, a submodule declared with `path = src` and URL
`https://dev.azure.com/example/platform/_git/ProductCore` is emitted as
`ProductCore`, while `git:path` and `git:submodule.path` remain `src`. The
resolved name is used consistently for `component.name`, `purl`, `bom-ref`, and
dependency references. If two components would otherwise have the same
reference, a deterministic checkout-path qualifier is added.

HTTP(S) credentials and URL query parameters are removed before URLs are written
to external references or properties. The version configuration is intentionally
separate: its submodule `name_pattern` continues to match the local checkout
folder (for example `src`), not the resolved component name (for example
`ProductCore`).

### Hierarchical Relationship Representation

The tool supports two approaches for representing the hierarchical relationships between repositories and their submodules:

#### 1. Dependencies Structure (Default)

By default, the tool uses the CycloneDX dependencies structure to represent hierarchical relationships. This approach:

- Emits the analyzed repository itself as `metadata.component` (the document's subject, per CycloneDX convention)
- Adds a unique `bom-ref` to each component
- Lists all submodules in a flat structure in the `components` array
- Uses a `dependencies` section to represent parent-child relationships (the root's entry references `metadata.component`'s `bom-ref`)
- Is fully compliant with the CycloneDX specification
- Makes the hierarchical relationships explicit and machine-readable

Example output with dependencies structure:

```json
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.4",
  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
  "version": 1,
  "metadata": {
    "timestamp": "2025-12-06T17:30:45Z",
    "tools": [
      {
        "vendor": "Janosch Meyer",
        "name": "sbom-git-sm",
        "version": "0.1.0"
      }
    ],
    "component": {
      "type": "application",
      "name": "main-repo",
      "version": "1.0.0",
      "purl": "pkg:github/user/main-repo@1.0.0",
      "bom-ref": "pkg:github/user/main-repo@1.0.0",
      "properties": [
        {
          "name": "git:branch",
          "value": "main"
        },
        {
          "name": "git:commit",
          "value": "abcdef1234567890"
        },
        {
          "name": "git:commit.short",
          "value": "abcdef12"
        },
        {
          "name": "git:path",
          "value": "."
        },
        {
          "name": "git:worktree.path",
          "value": "."
        },
        {
          "name": "git:tag",
          "value": "v1.0.0"
        },
        {
          "name": "version:source",
          "value": "tag"
        }
      ],
      "externalReferences": [
        {
          "type": "vcs",
          "url": "https://github.com/user/main-repo.git"
        }
      ]
    }
  },
  "components": [
    {
      "type": "library",
      "name": "submodule1",
      "version": "12345678",
      "purl": "pkg:github/user/submodule1@1234567890abcdef",
      "bom-ref": "pkg:github/user/submodule1@1234567890abcdef",
      "properties": [
        {
          "name": "git:branch",
          "value": "main"
        },
        {
          "name": "git:commit",
          "value": "1234567890abcdef"
        },
        {
          "name": "git:path",
          "value": "submodule1"
        },
        {
          "name": "git:worktree.path",
          "value": "submodule1"
        },
        {
          "name": "version:source",
          "value": "hash"
        }
      ],
      "externalReferences": [
        {
          "type": "vcs",
          "url": "https://github.com/user/submodule1.git"
        }
      ]
    }
  ],
  "dependencies": [
    {
      "ref": "pkg:github/user/main-repo@1.0.0",
      "dependsOn": [
        "pkg:github/user/submodule1@1234567890abcdef"
      ]
    }
  ]
}
```

#### 2. Nested Components (Alternative)

Alternatively, you can use the `--nested-components` flag to represent hierarchical relationships using nested components. This approach:

- Emits the analyzed repository itself as `metadata.component`; its direct submodules are the top-level `components` entries
- Nests deeper submodules within their parent components in a hierarchical structure
- Includes `bom-ref` fields for all components for validation compatibility
- Does not use the `dependencies` section
- May be more intuitive for visual inspection
- Represents the hierarchy directly in the component structure

Example output with nested components:

```json
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.4",
  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
  "version": 1,
  "metadata": {
    "timestamp": "2025-12-06T17:30:45Z",
    "tools": [
      {
        "vendor": "Janosch Meyer",
        "name": "sbom-git-sm",
        "version": "0.1.0"
      }
    ],
    "component": {
      "type": "application",
      "name": "main-repo",
      "version": "1.0.0",
      "purl": "pkg:github/user/main-repo@1.0.0",
      "bom-ref": "pkg:github/user/main-repo@1.0.0",
      "properties": [
        {
          "name": "git:branch",
          "value": "main"
        },
        {
          "name": "git:commit",
          "value": "abcdef1234567890"
        },
        {
          "name": "git:commit.short",
          "value": "abcdef12"
        },
        {
          "name": "git:path",
          "value": "."
        },
        {
          "name": "git:worktree.path",
          "value": "."
        },
        {
          "name": "git:tag",
          "value": "v1.0.0"
        },
        {
          "name": "version:source",
          "value": "tag"
        }
      ],
      "externalReferences": [
        {
          "type": "vcs",
          "url": "https://github.com/user/main-repo.git"
        }
      ]
    }
  },
  "components": [
    {
      "type": "library",
      "name": "submodule1",
      "version": "12345678",
      "purl": "pkg:github/user/submodule1@1234567890abcdef",
      "bom-ref": "pkg:github/user/submodule1@1234567890abcdef",
      "properties": [
        {
          "name": "git:branch",
          "value": "main"
        },
        {
          "name": "git:commit",
          "value": "1234567890abcdef"
        },
        {
          "name": "git:commit.short",
          "value": "12345678"
        },
        {
          "name": "git:path",
          "value": "submodule1"
        },
        {
          "name": "git:worktree.path",
          "value": "submodule1"
        },
        {
          "name": "version:source",
          "value": "hash"
        }
      ],
      "externalReferences": [
        {
          "type": "vcs",
          "url": "https://github.com/user/submodule1.git"
        }
      ],
      "components": [
        {
          "type": "library",
          "name": "nested-submodule",
          "version": "87654321",
          "purl": "pkg:github/user/nested-submodule@fedcba0987654321",
          "bom-ref": "pkg:github/user/nested-submodule@fedcba0987654321"
        }
      ]
    }
  ]
}
```

### Submodule Deduplication

**Nested components always mirror the physical checkout tree; the dependencies
structure represents a deduplicated component graph.**

If the same submodule (same resolved name and commit hash) is checked out at
more than one path in the tree -- a "diamond" dependency such as `root -> A ->
common` and `root -> B -> common` -- the two representations handle it
differently:

- **Dependencies structure (default):** a CycloneDX component is identified
  by its purl, and `common` at two paths is the same component, so it is
  emitted **once**, with both `A` and `B` listing it in their `dependsOn`
  (CycloneDX explicitly allows the dependency graph to be a DAG, not just a
  tree). No information is lost: every checkout path is kept as a repeated
  `git:path` / `git:worktree.path` property on the merged component, a
  differing branch or remote URL across checkouts is kept as an additional
  `git:branch` / `git:remote.url` / `git:origin.url` property, and
  `sbom-git-sm:occurrence.count` records how many checkouts were merged.
- **Nested components (`--nested-components`):** the structure itself *is*
  the physical tree, and a node cannot be nested under two parents at once,
  so both checkouts of `common` are still represented separately, each with
  its own `?path=`-qualified reference. This is unaffected by
  `--no-dedup-submodules`.

Two checkouts are only merged if they resolve to the exact same version. If a
version-extraction configuration ([Version Extraction](#version-extraction))
causes the two checkouts to report different versions, they are kept separate
(with the previous `?path=` qualifier) and a warning is issued, since merging
them would silently hide the discrepancy.

Use `--no-dedup-submodules` (or `dedup_submodules=False` in the Python API)
to restore the previous behavior everywhere: every checkout of a submodule
becomes its own component, disambiguated with a `?path=` qualifier.

## Using with OWASP Dependency-Track

The generated SBOMs import cleanly into [OWASP Dependency-Track](https://dependencytrack.org/) (DT). Recommendations for that use case:

- **Use the default mode** (flat `components` + `dependencies`). With
  `--nested-components` DT flattens the component tree and shows an empty
  dependency graph, since that mode emits no `dependencies` section.
- **Keep deduplication enabled** (the default). With `--no-dedup-submodules`,
  multiply-included submodules appear as separate components and distort
  component counts, policy hits, and audit effort.
- **Project mapping:** DT reads `metadata.component` for the project identity
  (e.g. `autoCreate` on BOM upload).
- **Vulnerability matching expectations:** public advisory sources (OSV, GitHub
  Advisories, OSS Index) key their data on package ecosystems (npm, Maven,
  PyPI, ...). Git repositories referenced as submodules rarely match public
  advisories, regardless of purl form. The namespaced purls and tag-based
  versions are still essential for DT's *internal* vulnerability workflow:
  define an internal vulnerability with the module's purl and a version range
  as affected component, and DT shows every project (product) containing that
  module in the affected range. Version ranges require ordable versions, so
  tag your module releases and make sure tags are present in the analyzed
  checkout (`git fetch --tags`).

## Known Limitations

- **No license information.** Git metadata does not carry license data, so
  components are emitted without a `licenses` field. License compliance
  checks in SBOM consumers (e.g. Dependency-Track's license policies) need
  license data from another source.

## Development

### Package Build Artifacts

When building the Python package, several files and directories are created that should not be tracked in version control:

- **Distribution files**: `dist/` directory containing wheel and source distribution files
- **Build files**: `build/` directory used during the build process
- **Metadata files**: `*.egg-info/` directories containing package metadata
- **Python cache files**: `__pycache__/` directories and `.pyc` files
- **Virtual environments**: `venv/`, `env/`, etc.

These files are automatically excluded from git by the `.gitignore` file.

### Building the Package

To build the package, run:

```bash
python -m pip install build
python -m build
```

This will create both wheel and source distributions in the `dist/` directory.

## Trademarks

Git and the Git logo are either registered trademarks or trademarks of Software Freedom Conservancy, Inc., corporate home of the Git Project, in the United States and/or other countries.

OWASP®, CycloneDX®, and Dependency-Track are trademarks or registered trademarks of the OWASP Foundation, Inc. GitHub® is a registered trademark of GitHub, Inc. Bitbucket® is a registered trademark of Atlassian Pty Ltd. GitLab® is a registered trademark of GitLab Inc. Microsoft®, Windows®, Azure®, and Visual Studio® are trademarks or registered trademarks of Microsoft Corporation. Python® and PyPI are trademarks or registered trademarks of the Python Software Foundation. Linux® is the registered trademark of Linus Torvalds in the U.S. and other countries. Apache Maven and Maven are trademarks of the Apache Software Foundation. npm® is a registered trademark of npm, Inc. Sonatype® and OSS Index are trademarks or registered trademarks of Sonatype, Inc.

All product names, logos, and brands are property of their respective owners and are used for identification purposes only. This project is not affiliated with or endorsed by any of them.

## License

This project is open source and available under the MIT License.

Copyright (c) 2025 Janosch Meyer (janosch.code@proton.me)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

This project was created with the assistance of artificial intelligence.
