The site that you are currently viewing is a static version of the Software Factory documentation delivered with the

version v7.2.
For up-to-date documentation, see the latest version.

SCA with JFrog Xray and Release Bundles for Maven Projects

Build Maven CI/CD pipeline with SBOMs, BuildInfo, Xray SCA policies, and Release Bundles

This page walks through building a Maven project using jf mvn instead of plain mvn generating SBOM with your packages, publishing a BuildInfo to Artifactory, scanning for vulnerabillities with Xray and Policy enforcement, and finally packaging everything into a Release Bundle .

How to read this page

The pipeline is built incrementally — each section adds one capability on top of the previous one.

If you want to jump straight to the final working pipeline, go to Complete pipeline at the bottom.

The intermediate steps are kept because understanding why each piece exists makes the whole easier to understand and operate.

Prerequisites

Before running the pipeline, the following must be in place:

  • Artifactory repositories in a Project where you have Project Admin rights
  • The cyclonedx-maven-plugin configured in your pom.xml
  • Xray indexing configured for both repositories and builds
  • GitLab CI variables and the custom Docker image

This section covers each in order.

Important

Xray indexing is the gate for everything that follows. Without it, the --scan flag on jf mvn does nothing, the Build Scans tab in Xray stays empty, and Watches never fire. Configure indexing before running the pipeline for the first time.

Artifactory repositories

If you do not have a JFrog Project, contact your platform for the creation procedure.

You need two Maven repositories to be accessible:

RepositoryTypePurpose
project-repo-snapshotsLocalSnapshot artifact storage
mavenVirtualDependency resolution (proxies central + local repos)

Configure the CycloneDX plugin in pom.xml

[...]
  <build>
        <plugins>
            <plugin>
                <groupId>org.cyclonedx</groupId>
                <artifactId>cyclonedx-maven-plugin</artifactId>
                <version>2.7.9</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>makeAggregateBom</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <projectType>library</projectType>
                    <schemaVersion>1.4</schemaVersion>
                    <includeBomSerialNumber>true</includeBomSerialNumber>
                    <includeCompileScope>true</includeCompileScope>
                    <includeProvidedScope>true</includeProvidedScope>
                    <includeRuntimeScope>true</includeRuntimeScope>
                    <includeSystemScope>true</includeSystemScope>
                    <includeTestScope>false</includeTestScope>
                    <outputFormat>all</outputFormat>
                    <outputName>bom</outputName>
                </configuration>
            </plugin>

Enable Xray indexing on repositories

Xray must be told which repositories to index. Without this step, artifacts deployed to Artifactory are invisible to Xray: no scan, no vulnerability report, no policy evaluation.

  1. From your Project home page, go to Administration > Xray > Indexed Resources > Repositories
  2. Click Add Repositories
  3. Select project-repo-snapshots and project-repo-releases
  4. Click Save

From this point, every artifact pushed to those repositories will be automatically indexed by Xray on arrival.

Note

Indexing an existing repository for the first time does not retroactively scan artifacts already present. Only newly pushed artifacts are scanned automatically. To scan existing artifacts, use Xray → Scans List → Scan Now on the repository.

Build indexing is a separate step configured after the first pipeline run, because the build name must exist in Artifactory before it can be selected in the UI. See Enable Xray indexing on builds below.

Tip

If you want Xray to index all builds rather than selecting them individually, navigate to Administration → Xray → Settings → Enable Indexing All Builds and toggle it on. This is convenient for new platforms but adds scan load — prefer explicit selection in production environments with many build names.

See the JFrog documentation for the full walkthrough: How to index and scan all builds in Xray

GitLab CI variables

The following variables must be defined in the GitLab project or group settings, with sensitive values masked:

VariableMaskedDescription
CICD_ARTIFACTORY_SERVERNoBase URL of your Artifactory instance
CICD_ARTIFACTORY_USERNAMENoService account username
CICD_ARTIFACTORY_APIKEYYesAccess token

Custom Docker image

The jobs use an image that bundles both the JFrog CLI and Maven 21. It can be built from a Dockerfile similar to the following:

FROM <artifactory.your-instance.example.com>/<repository>/maven/maven-java21/

USER root

RUN apk --no-cache add curl

WORKDIR $APP_TMP

ARG JFROG_CLI_VERSION=2.96.0
RUN curl -fSL "https://releases.jfrog.io/artifactory/jfrog-cli/v2-jf/${JFROG_CLI_VERSION}/jfrog-cli-linux-amd64/jf" \
        -o /usr/local/bin/jf && \
    chmod +x /usr/local/bin/jf

USER bob
RUN jf --version
WORKDIR /

The build_container job in the preparation stage builds this image from Dockerfile.build using the nextgen-cicd .docker step.

This image must be built and pushed before the build-publish stage runs.


The GitLab pipeline

Configure the pipeline with NextGen and variables

The pipeline uses shared templates from nextgen-cicd and follows a three-stage structure.

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

include:
  - project: nextgen-cicd/config
    ref: 'master'
    file: config.yml
  - project: nextgen-cicd/release
    ref: 'master'
    file:
      - 'step/maven.yml'
      - 'step/sonarqube.yml'
      - 'step/artifactory.yml'
      - 'step/hadolint.yml'
      - 'step/docker.yml'

variables:
  CICD_ARTIFACTORY_SERVER: https://artifactory.your-instance.example.com
  CICD_MAVEN_JAVA_VERSION: "21"
  CICD_MAVEN_TARGETS: "target/*"
  IMAGE_NAME: "my-example-java"
  MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"

stages:
  - preparation
  - quality-security
  - build-publish

The jf CLI configuration

All JFrog jobs share the same server registration pattern via a reusable hidden job:

.jfrog-configure:
  script:
    - jf --version
    - jf c add sf-server-id \
        --insecure-tls=false \
        --url=$CICD_ARTIFACTORY_SERVER \
        --user=$CICD_ARTIFACTORY_USERNAME \
        --access-token=$CICD_ARTIFACTORY_APIKEY
    - jf c use sf-server-id

Wire Maven to Artifactory

jf mvn-config replaces the server and repository references that would otherwise live in settings.xml. It tells the JFrog CLI which Artifactory server and repositories to use for dependency resolution and artifact deployment.

Note

The equivalent commands for other ecosystems follow the same pattern:

jf gradle-config, jf pip-config, jf docker, and so on. The--build-name and --build-number flags are available on all of them, and BuildInfo is recorded the same way.

before_script:
  - !reference [.jfrog-configure, script]
  - jf config show
  - jf mvn-config \
      --server-id-resolve=sf-server-id \
      --repo-resolve-releases=maven \
      --repo-resolve-snapshots=maven \
      --server-id-deploy=sf-server-id \
      --repo-deploy-releases=project-repo-releases \
      --repo-deploy-snapshots=project-repo-snapshots
FlagPurpose
--server-id-resolveServer to use when downloading dependencies
--repo-resolve-releasesVirtual repo for release dependency resolution
--repo-resolve-snapshotsVirtual repo for snapshot dependency resolution
--server-id-deployServer to use when uploading artifacts
--repo-deploy-releasesTarget repo for release artifacts
--repo-deploy-snapshotsTarget repo for snapshot artifacts
Important

jf mvn-config must run before jf mvn. If you skip this step, jf mvn falls back to the distributionManagement block in pom.xml and BuildInfo recording still works, but dependency resolution is not routed through Artifactory.


Run the pipeline

Build and deploy with BuildInfo recording

At this stage the goal is simple: replace mvn clean install with jf mvn clean install and understand what changes.

mvn-build-with-jf:
  image: $CI_REGISTRY_IMAGE/jf-cli-with-maven:latest
  stage: build-publish
  before_script:
    - !reference [.jfrog-configure, script]
    - jf mvn-config \
        --server-id-resolve=sf-server-id \
        --repo-resolve-releases=maven \
        --repo-resolve-snapshots=maven \
        --server-id-deploy=sf-server-id \
        --repo-deploy-releases=project-repo-releases \
        --repo-deploy-snapshots=project-repo-snapshots
  script:
    - jf mvn clean install \
        --build-name=$CI_PROJECT_NAME \
        --build-number=$CI_PIPELINE_IID \
        --project=project-key
  artifacts:
    reports:
      coverage_report:
        coverage_format: jacoco
        path: target/site/jacoco/jacoco.xml

What jf mvn does differently from mvn

jf mvn is a thin wrapper around Maven that intercepts the build lifecycle to:

  • Route all dependency downloads through the configured Artifactory virtual repo
  • Record every resolved dependency (including the actual resolved version, not just the declared one) into a local BuildInfo accumulator
  • Record every deployed artifact (JAR, POM, SBOM files) into the same accumulator
Important

The --build-name and --build-number flags are the BuildInfo identity key. They must be consistent across all jf commands in the same pipeline run.

The CycloneDX SBOMs are uploaded automatically

Because pom.xml configures the cyclonedx-maven-plugin to run at the package phase, jf mvn clean install produces target/bom.json and target/bom.xml before the deploy phase. The JFrog CLI then deploys them to Artifactory alongside the JAR — no extra upload step is needed.

In the pipeline log you will see the Xray indexer pick them up immediately:

[Info] [Thread 1] Indexing file: target/bom.json
[Info] [Thread 2] Indexing file: target/bom.xml
[Info] [Thread 0] Indexing file: target/greenarrow-1.8.0-SNAPSHOT.jar
[Info] Waiting for scan to complete on JFrog Xray...
[Info] Scan completed successfully.
{
  "status": "success",
  "totals": { "success": 4, "failure": 0 }
}
Note

success: 4 means four artifacts were indexed: the JAR, both SBOM files, and the POM. The scan result here is informational only — no policy is enforced yet. The pipeline succeeds regardless of findings at this stage.

Publish BuildInfo

After the build, the accumulated BuildInfo exists only locally. jf rt bp publishes it to Artifactory so it becomes queryable by Xray and visible in the UI.

Add the following line to the script section after jf mvn:

script:
  - [...]
  - jf mvn clean install \
      --build-name=$CI_PROJECT_NAME \
      --build-number=$CI_PIPELINE_IID \
      --project=project-key
  - jf rt bp $CI_PROJECT_NAME $CI_PIPELINE_IID \
      --project=project-key \
      --collect-env \
      --collect-git-info \
      --build-url=$CI_JOB_URL \
      --env-exclude="*PASS*;*PAT*;*secret*;*key*;*TOKEN*;*AUTH*"
FlagWhat it records
--collect-envAll CI environment variables (filtered by --env-exclude)
--collect-git-infoGit commit SHA, branch, remote URL
--build-urlDirect link back to the GitLab job log
--env-excludeGlob patterns for variables to strip before publishing
Important

--env-exclude is a security hygiene step. Without it, secrets injected as CI variables (tokens, passwords, API keys) would be stored in plain text inside the BuildInfo JSON in Artifactory. The pattern *PASS*;*PAT*;*secret*;*key*;*TOKEN*;*AUTH* covers the most common naming conventions — review and extend it to match your organisation’s variable naming.

Viewing BuildInfo in Artifactory

After jf rt bp completes, navigate to: Artifactory → Builds → my-project-java<pipeline IID>

The Build Info page shows:

  • Modules — the Maven modules built in this run
  • Artifacts — the JAR, POM, and SBOM files deployed, with their checksums
  • Dependencies — every resolved dependency with its actual resolved version
  • Properties — the environment variables collected (filtered)
  • Git — commit SHA, branch, remote

The Dependencies tab is where the declared vs resolved version gap becomes visible. If pom.xml declares jackson-databind:2.9.10.8 but Maven resolved 2.15.2 via a BOM, the resolved version is what appears here — not the declared one.

Enable Xray indexing on builds

Now that a BuildInfo has been published, build indexing can be configured.

Repository indexing (configured earlier) covers artifacts pushed to Artifactory. Build indexing is a separate configuration that tells Xray to index and scan BuildInfo records published by jf rt bp. Without it, the Build Scans tab remains empty even after a successful pipeline run.

From your Project home page:

  1. Go to Administration > Xray > Indexed Resources > Builds
  2. Click Add Builds
  3. Select the build name — this matches $CI_PROJECT_NAME in the pipeline
  4. Click Save

After this, every BuildInfo published under that build name will be automatically indexed and scanned by Xray.

Observe findings in Xray (no enforcement yet)

With BuildInfo published and build indexing enabled, Xray produces a build-level security report. No pipeline change is needed at this stage — the scan already ran during jf mvn thanks to the indexer. The goal here is to understand the exposure before enforcing anything.

From your Project home page, navigate to Xray > Scans List > Builds > my-project-java

This is the Build Scans tab, which is different from the Binary Scans tab used by jf scan. The build report shows vulnerabilities scoped to this specific build — its dependencies, its artifacts, and the SBOMs produced during the build.

What to look for

  • The severity breakdown — how many Critical, High, Medium and Low findings
  • Which components carry the findings
  • Whether the findings align with what jf audit reported earlier — differences indicate version resolution gaps between declared and bundled dependencies
Note

Spend time on this view before configuring any enforcement. Understanding the baseline — what is there, how severe, which components — makes it easier to set meaningful thresholds and handle exceptions deliberately rather than reactively.


Configure a Policy and Watch in Xray

With the baseline understood, the next step is to define what constitutes a violation and scope it to this build.

There is a generic policies/watches howto to explore more.

Create a Security Policy

From your Project home page, navigate to Xray > Policies > New Policy

FieldSuggested value
Nameproject-key-security-policy
Policy TypeSecurity
Rule nameblock-critical
Rule TypeCVE
Rule CategoryMinimal Severity
Minimum severityCritical
Action (then)Fail build / Block download

Add a second rule if you want to warn on High without failing:

FieldSuggested value
Rule namewarn-high
Minimum severityHigh
ActionGenerate violation (notify only)

Save the policy.

Create a Watch scoped to the build

From your Project home page, navigate to Xray > Watches > New Watch

FieldSuggested value
Namemy-project-build-watch
Resources typeBuilds
Build namemy-project-java
Assigned policiesproject-key-security-policy

Save and activate the Watch.

Note

Scoping the Watch to a specific build name rather than a whole repository means only artifacts produced by this build trigger the policy.

You can also select all builds, to include future builds.

Enforce the policy in CI

With the Watch active, add the --scan and --fail flags to jf mvn:

script:
  - [...]
  - jf mvn clean install \
      --build-name=$CI_PROJECT_NAME \
      --build-number=$CI_PIPELINE_IID \
      --project=project-key \
      --scan \
      --fail
  - jf rt bp $CI_PROJECT_NAME $CI_PIPELINE_IID \
      --project=project-key \
      --collect-env \
      --collect-git-info \
      --build-url=$CI_JOB_URL \
      --env-exclude="*PASS*;*PAT*;*secret*;*key*;*TOKEN*;*AUTH*"

Incremental adoption with allow_failure

Introducing --fail on an existing project that already has known vulnerabilities will immediately break every pipeline run.

mvn-build-with-jf:
  script:
    - jf mvn clean install \
        --build-name=$CI_PROJECT_NAME \
        --build-number=$CI_PIPELINE_IID \
        --project=project-key \
        --scan \
        --fail
    - jf rt bp ...

Once the team has addressed the Critical findings — or created deliberate Xray ignore rules for accepted risks — set allow_failure: false to enforce the gate fully.

Tip

Use Xray ignore rules (scoped to a CVE + component + time-limited expiry) for vulnerabilities that have no available fix or have been assessed as not exploitable in your deployment context. This keeps the policy meaningful without blocking work indefinitely on unfixable CVEs.


Create a Release Bundle from BuildInfo

A Release Bundle is created manually after the build has passed the required verification and qualification phases — functional testing, integration testing, and promotion from snapshot to release. This deliberate human gate ensures that only validated builds become release candidates.

  1. Navigate to the Builds section in your Project under Artifactory
  2. Select Create Release Bundle at the top right corner of the screen
  3. In the New Release Bundle screen, fill in:
    • Release Bundle Name — enter a name for your release bundle
    • Release Bundle Version — specify the version using Semver convention (e.g., 1.0.0)
    • Signing Key — select an available signing key from the dropdown list
  4. Select a Build Name and Build Version from the list of available builds:
    • Check Include Build Dependencies to include all dependencies associated with the build
    • Select Add and repeat to include additional builds if needed
  5. Select Create to finalise the creation of the release bundle

Complete pipeline

The following is the full .gitlab-ci.yml incorporating all steps. Copy and adapt it to your project — replace project-key, project-repo-releases, project-repo-snapshots, and my-project-java with your actual values.

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

include:
  - project: nextgen-cicd/config
    ref: 'master'
    file: config.yml
  - project: nextgen-cicd/release
    ref: 'master'
    file:
      - 'step/maven.yml'
      - 'step/sonarqube.yml'
      - 'step/artifactory.yml'
      - 'step/hadolint.yml'
      - 'step/docker.yml'

variables:
  CICD_SONAR_AUTH_TOKEN: $SONAR_TOKEN
  CICD_SONAR_PROJECT_KEY: $CICD_MAVEN_SONAR_PROJECTKEY
  CICD_ARTIFACTORY_SERVER: https://artifactory.your-instance.example.com
  MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
  CICD_MAVEN_JAVA_VERSION: "21"
  CICD_MAVEN_TARGETS: "target/*"
  IMAGE_NAME: "my-project-java"

stages:
  - preparation
  - quality-security
  - build-publish

# ── Shared anchor ─────────────────────────────────────────────────────────────

.jfrog-configure:
  script:
    - jf --version
    - jf c add sf-server-id \
        --insecure-tls=false \
        --url=$CICD_ARTIFACTORY_SERVER \
        --user=$CICD_ARTIFACTORY_USERNAME \
        --access-token=$CICD_ARTIFACTORY_APIKEY
    - jf c use sf-server-id

# ── Preparation ───────────────────────────────────────────────────────────────

build_container:
  variables:
    CICD_DOCKER_TAG_NAME: $CI_REGISTRY_IMAGE/jf-cli-with-maven:latest
    CICD_DOCKERFILE: Dockerfile.build
  stage: preparation
  extends: .docker

# ── Quality & Security ────────────────────────────────────────────────────────

code-quality-scan:
  stage: quality-security
  extends: .maven-sonar-scan
  variables:
    CICD_MAVEN_SONAR_AUTHTOKEN: $SONAR_TOKEN
    CICD_MAVEN_SONAR_SOURCES: "src/main"
    CICD_MAVEN_SONAR_PROJECTKEY: "sonar-project-key"
    CICD_MAVEN_SONAR_URL: "https://sonarqube.your-instance.example.com/"
    CICD_MAVEN_SONAR_GATECHECK: "true"
    CICD_MAVEN_EXTRA_ARGS: 'verify'
  artifacts:
    paths:
      - target/sonar/report-task.txt
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# ── Build & Publish ───────────────────────────────────────────────────────────

mvn-build-with-jf:
  image: $CI_REGISTRY_IMAGE/jf-cli-with-maven:latest
  stage: build-publish
  before_script:
    - !reference [.jfrog-configure, script]
    - jf config show
    - jf mvn-config \
        --server-id-resolve=sf-server-id \
        --repo-resolve-releases=maven \
        --repo-resolve-snapshots=maven \
        --server-id-deploy=sf-server-id \
        --repo-deploy-releases=project-repo-releases \
        --repo-deploy-snapshots=project-repo-snapshots
  script:
    # Build, deploy artifacts + SBOMs, record BuildInfo, scan on deploy
    - jf mvn clean install \
        --build-name=$CI_PROJECT_NAME \
        --build-number=$CI_PIPELINE_IID \
        --project=project-key \
        --scan \
        --fail
    # Publish BuildInfo with environment and Git context
    - jf rt bp $CI_PROJECT_NAME $CI_PIPELINE_IID \
        --project=project-key \
        --collect-env \
        --collect-git-info \
        --build-url=$CI_JOB_URL \
        --env-exclude="*PASS*;*PAT*;*secret*;*key*;*TOKEN*;*AUTH*"
  artifacts:
    reports:
      coverage_report:
        coverage_format: jacoco
        path: target/site/jacoco/jacoco.xml
  allow_failure: true   # set to false once Critical findings are resolved