Add CycloneDX exporter

What does this MR do and why?

This MR adds a new exporter to export the dependency list in CycloneDX format. It conforms to the structure described in #524733 (comment 2396325960).

Click to expand requirements

The SBoM will have the following JSON:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "serialNumber": "urn:uuid:c13b4efe-dd33-4975-8733-30b845b897e0",
  "version": 1,
  "metadata": {
    "timestamp": "2025-03-01T20:21:30Z",
    "tools": [
      {
        "vendor": "GitLab",
        "name": "GitLab dependency list export",
        "version": "17.11.0"
      }
    ],
    "authors": [
      {
        "name": "GitLab",
        "email": "support@gitlab.com"
      }
    ],
    "component": {
      "type": "application",
      "name": "project-name",
      "externalReferences": [
        {
          "type": "vcs",
          "url": "https://gitlab.com/path/to/project"
        }
      ]
    }
  },
  "components": [
    {
      "name": "CFPropertyList",
      "version": "3.0.7",
      "purl": "pkg:gem/CFPropertyList@3.0.7",
      "type": "library",
      "bom-ref": "pkg:gem/CFPropertyList@3.0.7",
      "licenses": [
        {
          "license": {
            "name": "MIT"
          }
        }
      ]
    },
    {
      "name": "RedCloth",
      "version": "4.3.4",
      "purl": "pkg:gem/RedCloth@4.3.4",
      "type": "library",
      "bom-ref": "pkg:gem/RedCloth@4.3.4",
      "licenses": [
        {
          "license": {
            "name": "MIT"
          }
        }
      ]
    },
    ...
  ]
}

The specification describes the requirements for each field. Here are my notes on how we will populate each, where applicable.

  1. bomFormat will be hard-coded to CycloneDX.
  2. specVersion will be set by the exporter to the relevant specification version. The current iteration will be implemented in CycloneDX 1.6. When we add support for new specification versions, those will come with a new exporter and export_type.
  3. serialNumber will be a randomly generated serial number conforming to RFC 4122.
  4. version will be hard-coded to 1. This number is supposed to be incremented if modifications are made to a BoM without generating a new serial number. Since we will generate a new serial number for each export, we don't need to worry about version tracking.
  5. metadata.timestamp will be set to the current time in UTC. For the most accuracy, the timestamp should be set around the same time that the sbom_occurrences are queried. Since we will be querying the data in batches, there is a risk that the data could be changed by a running pipeline as we are generating the export. This could be solved by using locking so that sbom ingestion and sbom export block each other and cannot happen at the same time. However, I think it's unlikely enough that we can accept the risk for now.
  6. metadata.tools[].version will be set to the current version of the GitLab instance.
  7. metadata.component will use the name and URL of the project in GitLab.
  8. components[].bom-ref will be set to the purl if present. Otherwise, it will be set to a randomly generated serial number.
  9. All other components fields directly map to a column on the sbom_occurrences records except for the purl which can be generated from multiple columns.

This MR is part of a stack:

  1. ➡️ Add CycloneDX exporter (!185516 - merged) (You are here)
  2. Add cylonedx export parameter (!185517 - merged)
  3. Add dropdown item for cyclonedx export (!185518 - merged)

SQL

Get start ID: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/37681/commands/115131

SELECT
    sbom_occurrences.id
FROM
    sbom_occurrences
WHERE
    sbom_occurrences.project_id = 278964
ORDER BY
    sbom_occurrences.id ASC
LIMIT 1;

Get end ID: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/37681/commands/115132

SELECT
    sbom_occurrences.id
FROM
    sbom_occurrences
WHERE
    sbom_occurrences.project_id = 278964 AND
    sbom_occurrences.id >= 5899526150
ORDER BY
    sbom_occurrences.id ASC
LIMIT 1
OFFSET 1000;

Get batch: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/37681/commands/115133

SELECT
    sbom_occurrences.*
FROM
    sbom_occurrences
WHERE
    sbom_occurrences.project_id = 278964 AND
    sbom_occurrences.id >= 5899526150 AND
    sbom_occurrences.id < 5899527683;

References

Screenshots or screen recordings

Before After

How to set up and validate locally

  1. Set up the dependency list on a project

  2. Run this code in the Rails console:

    project = Project.find(<project_id>)
    export_stub = Struct.new(:project, :author).new(project, User.first)
    content = Sbom::Exporters::Cyclonedx::V16Service.new(export_stub, project.sbom_occurrences).generate
    pp Gitlab::Json.parse(content)

Sample output: dependency-list.cdx.json

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Brian Williams

Merge request reports

Loading