TigerGate scans the libraries your project uses and tells you which ones have
known security problems.
To do that, it first needs a list of your libraries and their exact version
numbers. For most languages that list is already sitting in your repository, so
everything works out of the box. For Java, Gradle, and some .NET projects,
that list does not exist yet — it has to be worked out by running your build
tool. If your build tool is not installed, TigerGate cannot get the list, and
your libraries are never checked.
When this happens, nothing looks broken. The scan finishes, the build passes,
and the report says no problems were found. The libraries were simply never
checked. Find your language in the table below before you trust a clean
result.
Find your language
We tested each of these with a real project.
| Your language | Works out of the box? | What you need to do |
|---|
| Node.js | Yes | Commit package-lock.json (or yarn.lock / pnpm-lock.yaml) |
| Python | Yes | Commit requirements.txt, poetry.lock, or Pipfile.lock |
| Go | Yes | Commit go.mod and go.sum |
| PHP | Yes | Commit composer.lock |
| Ruby | Yes | Commit Gemfile.lock |
| .NET | Yes, if you commit packages.lock.json | Turn on the lock file — it takes one line |
| .NET | No, if you only have a .csproj | Turn on the lock file, or install the .NET SDK |
| Java (Maven) | No | Install Java and Maven |
| Java (Gradle) | No | Install Java and Gradle |
If your language says “Yes”, the standard pipeline in
Code scanning in CI/CD already works. You
can stop reading here.
Why Java projects report no problems
This is the most common surprise, so it is worth seeing exactly what happens.
Spring Boot projects do not write version numbers next to their libraries. They
inherit them from a parent file instead:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<!-- notice: no version here -->
</dependency>
</dependencies>
Working out the real version numbers means reading that parent file, and only
Maven knows how to do that. Without Java installed, TigerGate sees the library
names but not their versions:
NAME VERSION
spring-boot-starter-log4j2 UNKNOWN
spring-boot-starter-web UNKNOWN
TigerGate cannot tell you whether “UNKNOWN” is a safe version or a dangerous
one, so it reports nothing:
That project is not safe. Spring Boot 2.5.4 actually pulls in Log4j 2.14.1
— the version behind Log4Shell, one of the most exploited vulnerabilities ever
found. Once Java and Maven are installed, the same project correctly reports it
as Critical.
If you write an exact version next to every library in your pom.xml, those
libraries are checked correctly without Java. But the libraries they pull in
behind the scenes are still missed, and that is where most problems hide.
If you see this error
The JAVA_HOME environment variable is not defined correctly,
this environment variable is needed to run this program.
Maven is installed but Java is not, or JAVA_HOME points somewhere that is not
a real Java installation. The fix is below.
TigerGate runs your build tool itself, so the build tool has to live in the
same place as TigerGate. Pointing at a copy of Maven elsewhere on the machine
will not work.
The simplest way is to make your own version of the scanner image with the build
tool added. Copy the file for your language, build it once, and push it to a
registry your CI can reach.
Java with Maven
Java with Gradle
.NET
FROM tigergate/tigergate-cli:latest
# Copy Java in
COPY --from=eclipse-temurin:17-jdk /opt/java/openjdk /opt/java/openjdk
ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="/opt/java/openjdk/bin:${PATH}"
# Copy Maven in
COPY --from=maven:3.9-eclipse-temurin-17 /usr/share/maven /usr/share/maven
ENV PATH="/usr/share/maven/bin:${PATH}"
ENTRYPOINT ["tigergate"]
We built and tested this exact file: Java 17.0.19, Maven 3.9.16. It makes
the image about 290 MB bigger.It copies Java from the official Java image rather than installing it with
apt, because that keeps the version predictable and does not break when
the base image is updated.FROM tigergate/tigergate-cli:latest
COPY --from=eclipse-temurin:17-jdk /opt/java/openjdk /opt/java/openjdk
ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="/opt/java/openjdk/bin:${PATH}"
COPY --from=gradle:8-jdk17 /opt/gradle /opt/gradle
ENV PATH="/opt/gradle/bin:${PATH}"
ENTRYPOINT ["tigergate"]
If your project has a gradlew file, you only need the Java part. The
gradlew file downloads Gradle by itself the first time it runs.FROM tigergate/tigergate-cli:latest
COPY --from=mcr.microsoft.com/dotnet/sdk:8.0 /usr/share/dotnet /usr/share/dotnet
ENV DOTNET_ROOT=/usr/share/dotnet
ENV PATH="/usr/share/dotnet:${PATH}"
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
ENTRYPOINT ["tigergate"]
Most .NET teams do not need this. Committing a lock file is easier and
works better — see .NET below.
Build it and push it:
docker build -t registry.example.com/tigergate-cli-java:1.0.0 .
docker push registry.example.com/tigergate-cli-java:1.0.0
Then use that image name in your pipeline wherever the examples say
tigergate/tigergate-cli.
Java (Maven)
Pick your CI system and copy the file.
Each pipeline does three things: download your libraries, scan them, and fail
the build if the scan finds something bad. On a pull request it checks only the
code you changed. On your main branch it checks everything.
GitHub Actions
GitLab CI
Azure Pipelines
Bitbucket Pipelines
name: TigerGate Security
on:
push:
branches: [main]
pull_request:
env:
TIGERGATE_API_KEY: ${{ secrets.TIGERGATE_API_KEY }}
jobs:
scan:
runs-on: ubuntu-latest
container:
image: registry.example.com/tigergate-cli-java:1.0.0
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
permissions:
contents: read
pull-requests: write
statuses: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
# Remember downloaded libraries so the next run is faster
- uses: actions/cache@v4
with:
path: ~/.m2/repository
key: m2-${{ hashFiles('**/pom.xml') }}
restore-keys: m2-
# Download the libraries
- run: mvn -B -q dependency:go-offline
# Scan
- run: tigergate scan --type all --scan-scope diff --upload
if: github.event_name == 'pull_request'
- run: tigergate scan --type all --scan-scope full --upload
if: github.event_name == 'push'
variables:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: "safe.directory"
GIT_CONFIG_VALUE_0: "*"
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
.tigergate:
image: registry.example.com/tigergate-cli-java:1.0.0
cache:
key:
files: [pom.xml]
paths:
- .m2/repository
before_script:
- mvn -B -q dependency:go-offline
merge-request-scan:
extends: .tigergate
script:
- tigergate scan --type all --scan-scope diff --upload
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
main-branch-scan:
extends: .tigergate
script:
- tigergate scan --type all --scan-scope full --upload
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
The three GIT_CONFIG lines stop Git complaining that the checked-out files
belong to a different user. GitLab needs them; most others do not.trigger:
branches: { include: [main] }
pr:
branches: { include: ['*'] }
pool:
vmImage: ubuntu-latest
container:
image: registry.example.com/tigergate-cli-java:1.0.0
endpoint: my-registry-service-connection
variables:
- group: tigergate
- name: MAVEN_CACHE_FOLDER
value: $(Pipeline.Workspace)/.m2/repository
steps:
- checkout: self
fetchDepth: 0
- task: Cache@2
inputs:
key: 'maven | "$(Agent.OS)" | **/pom.xml'
restoreKeys: 'maven | "$(Agent.OS)"'
path: $(MAVEN_CACHE_FOLDER)
- script: mvn -B -q -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER) dependency:go-offline
displayName: Download libraries
- script: |
if [ "$(Build.Reason)" = "PullRequest" ]; then SCOPE=diff; else SCOPE=full; fi
tigergate scan --type all --scan-scope $SCOPE --upload
displayName: TigerGate scan
env:
TIGERGATE_API_KEY: $(TIGERGATE_API_KEY)
image:
name: registry.example.com/tigergate-cli-java:1.0.0
username: $REGISTRY_USER
password: $REGISTRY_TOKEN
definitions:
caches:
maven2: ~/.m2/repository
pipelines:
pull-requests:
'**':
- step:
name: TigerGate scan (changed code)
caches: [maven2]
clone:
depth: full
script:
- mvn -B -q dependency:go-offline
- tigergate scan --type all --scan-scope diff --upload
branches:
main:
- step:
name: TigerGate scan (everything)
caches: [maven2]
clone:
depth: full
script:
- mvn -B -q dependency:go-offline
- tigergate scan --type all --scan-scope full --upload
Java (Gradle)
The same idea, using the Gradle image. Use ./gradlew if your project has one.
GitHub Actions
GitLab CI
Azure Pipelines
Bitbucket Pipelines
jobs:
scan:
runs-on: ubuntu-latest
container:
image: registry.example.com/tigergate-cli-gradle:1.0.0
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: gradle-
- run: ./gradlew --no-daemon dependencies
- run: tigergate scan --type all --scan-scope full --upload
env:
TIGERGATE_API_KEY: ${{ secrets.TIGERGATE_API_KEY }}
gradle-scan:
image: registry.example.com/tigergate-cli-gradle:1.0.0
variables:
GRADLE_USER_HOME: $CI_PROJECT_DIR/.gradle
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: "safe.directory"
GIT_CONFIG_VALUE_0: "*"
cache:
paths: [.gradle/caches]
script:
- ./gradlew --no-daemon dependencies
- tigergate scan --type all --scan-scope full --upload
container:
image: registry.example.com/tigergate-cli-gradle:1.0.0
endpoint: my-registry-service-connection
steps:
- checkout: self
fetchDepth: 0
- task: Cache@2
inputs:
key: 'gradle | "$(Agent.OS)" | **/*.gradle*'
path: $(HOME)/.gradle/caches
- script: ./gradlew --no-daemon dependencies
- script: tigergate scan --type all --scan-scope full --upload
env:
TIGERGATE_API_KEY: $(TIGERGATE_API_KEY)
image:
name: registry.example.com/tigergate-cli-gradle:1.0.0
pipelines:
branches:
main:
- step:
caches: [gradle]
clone:
depth: full
script:
- ./gradlew --no-daemon dependencies
- tigergate scan --type all --scan-scope full --upload
.NET
You have two options. The first is easier and gives better results.
Best: turn on the lock file
Add this line to your .csproj:<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
Then run dotnet restore once and commit the file it creates:dotnet restore
git add packages.lock.json
git commit -m "Add NuGet lock file"
That file lists every library and its exact version, including the ones
pulled in behind the scenes. TigerGate reads it directly, so you do not need
the .NET SDK on your CI machine and you can use the normal scanner image. Otherwise: install the .NET SDK
If you cannot commit a lock file, use the .NET image from above and run
dotnet restore before the scan.
GitHub Actions
GitLab CI
Azure Pipelines
Bitbucket Pipelines
jobs:
scan:
runs-on: ubuntu-latest
container:
image: tigergate/tigergate-cli:latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: tigergate scan --type all --scan-scope full --upload
env:
TIGERGATE_API_KEY: ${{ secrets.TIGERGATE_API_KEY }}
dotnet-scan:
image: tigergate/tigergate-cli:latest
variables:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: "safe.directory"
GIT_CONFIG_VALUE_0: "*"
script:
- tigergate scan --type all --scan-scope full --upload
container:
image: tigergate/tigergate-cli:latest
steps:
- checkout: self
fetchDepth: 0
- script: tigergate scan --type all --scan-scope full --upload
env:
TIGERGATE_API_KEY: $(TIGERGATE_API_KEY)
image: tigergate/tigergate-cli:latest
pipelines:
branches:
main:
- step:
clone:
depth: full
script:
- tigergate scan --type all --scan-scope full --upload
These examples assume you committed the lock file. If you did not, change the
image to your .NET one and add dotnet restore --use-lock-file before the scan.
Node.js, Python, Go, Ruby, PHP
Nothing to install. Just make sure the right file is committed to Git.
| Language | Commit this file | If it is missing |
|---|
| Node.js | package-lock.json, yarn.lock, or pnpm-lock.yaml | package.json gives version ranges like ^4.17.0, not real versions |
| Python | requirements.txt, poetry.lock, or Pipfile.lock | Pin exact versions; >= ranges cannot be checked |
| Go | go.mod and go.sum | — |
| Ruby | Gemfile.lock | Gemfile alone is not enough |
| PHP | composer.lock | composer.json alone is not enough |
The rule is the same everywhere: a file saying “some version of this library”
cannot be checked. A file saying “exactly version 4.17.15” can.
If one of these reports nothing, check that the file is really in Git and not
excluded by .gitignore.
Common problems
| What you see | What it means | How to fix it |
|---|
JAVA_HOME is not defined correctly | Java is not installed, or JAVA_HOME points to the wrong place | Use the Java image above — it sets this for you |
Spring Boot project says No vulnerabilities found | Version numbers were never worked out, so nothing could be checked | Install Java and Maven, and run mvn dependency:go-offline before scanning |
Libraries listed with version UNKNOWN | Same problem as above | Same fix |
| .NET project shows no libraries | There is no lock file and no .NET SDK | Commit packages.lock.json |
| Gradle project shows no libraries | build.gradle is not a list of exact versions | Use the Gradle image and run ./gradlew dependencies |
mvn: not found | The job is using the normal scanner image | Switch it to your Java image |
| Works on my laptop, not in CI | The download step and the scan step ran in different places | Both must run in the same job, using the same image |
| Every run is slow | Libraries are downloaded again each time | Add the cache section shown for your CI system |