GitLab + Checkmarx
Complete Setup Guide
Every push and MR automatically triggers a Checkmarx scan. Results appear natively inside GitLab — no manual trigger, no webhook, no polling required.
What you're building
A fully automated security scanning pipeline. Once set up, no developer needs to do anything — pushing code is enough to trigger a scan.
Architecture
GitLab CI drives everything. The runner invokes CxFlow which talks to Checkmarx. Checkmarx never reaches back into GitLab — clean, firewall-friendly.
Create Checkmarx Service Account
This is done by whoever administers your Checkmarx instance. Do not use a personal account or the built-in cxadmin — create a dedicated service account.
1 — Log into Checkmarx Web UI as admin
https://checkmarx.company.local
Use the cxadmin account or any account with User Management permissions.
2 — Navigate to User Management
Top menu → Administration → User Management
3 — Create new user — fill in these exact values
| Field | Value |
|---|---|
| Username | svc-gitlab-ci |
| Password | Generate 20+ chars — save it immediately in a password manager |
| Confirm Password | same as above |
A team mailbox e.g. devops@company.com | |
| First Name | GitLab |
| Last Name | CI Service |
| Account Active | ✓ checked |
| Password never expires | ✓ checked — critical, see warning below |
4 — Assign roles — Scanner only
On the same screen under Roles:
| Role | Set |
|---|---|
SAST Scanner | ✓ check this |
Admin | ✗ leave unchecked |
Data Analyst | ✗ leave unchecked |
Manager | ✗ leave unchecked |
5 — Click Save
The account is now created. Move to Step 02 to verify it works before touching GitLab.
Verify the Checkmarx Account
Run this from the GitLab Runner host or any machine that can reach Checkmarx on port 443. Do this before setting any GitLab variables.
curl -X POST https://checkmarx.company.local/cxrestapi/auth/identity/connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=svc-gitlab-ci" \ -d "password=<the-password-you-set>" \ -d "grant_type=password" \ -d "scope=sast_rest_api" \ -d "client_id=resource_owner_client" \ -d "client_secret=014DF517-39D1-4453-B7B3-9930C563627C"
{"access_token":"eyJhbGci...","token_type":"Bearer","expires_in":3599}You have a valid token. The account works. Proceed to Step 03.
If it fails
| Error | Cause | Fix |
|---|---|---|
401 Unauthorized | Wrong password or account not saved correctly | Reset password in Checkmarx User Management |
Connection refused | Network / firewall issue — not an account problem | Verify port 443 is open between runner and Checkmarx |
400 Bad Request | Missing or malformed POST body fields | Check all -d params are present and URL-encoded |
Create GitLab API Token
You generate this yourself in GitLab. CxFlow uses it to post scan results back to MRs and the Security Dashboard.
1 — Navigate to Access Tokens
GitLab → Top-right avatar → Edit Profile → Access Tokens → Add new token
2 — Fill in these values
| Field | Value |
|---|---|
| Token name | cxflow-scanner |
| Expiry date | 90 days from today — set a calendar reminder to rotate |
| Scopes | api only — everything else unchecked |
3 — Click Create token
Store the token in a password manager or Vault right now. You'll paste it into GitLab CI/CD Variables in the next step.
Set Group-Level CI/CD Variables
Set these at the group level — not per-project. Every repo under that group inherits them automatically. You only do this once.
1 — Navigate to Variables
GitLab → Your Group → Settings → CI/CD → Variables → Expand → Add variable
2 — Add these four variables exactly
| Variable name | Value | Protected | Masked |
|---|---|---|---|
CX_SERVER | e.g. https://checkmarx.company.local | No | No |
CX_USERNAME | svc-gitlab-ci | Yes | No |
CX_PASSWORD | the password you set in Step 01 | Yes | Yes |
GITLAB_TOKEN | the token you copied in Step 03 | Yes | Yes |
Masked = value is redacted from job logs even if accidentally printed. Always mask passwords and tokens.
https://checkmarx.company.local ✓https://checkmarx.company.local/ ✗ — CxFlow will build malformed API URLs.
Add .gitlab-ci.yml to the Repo
Create this file in the root of every repo you want scanned. If you want it on all repos without touching each one, use GitLab's Compliance Pipelines or include: from a central project.
# Fire on every push to any branch AND on every MR workflow: rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH' stages: - sast checkmarx-sast: stage: sast # Pin by SHA256 digest — never use :latest (see Step 06) image: checkmarx/cx-flow@sha256:<your-pinned-digest> variables: # Full scan on default branch, incremental (changed files only) on everything else IS_INCREMENTAL: >- $([[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]] && echo false || echo true) script: - | java -jar /app/cx-flow.jar \ --scan \ --cx-server="${CX_SERVER}" \ --cx-user="${CX_USERNAME}" \ --cx-password="${CX_PASSWORD}" \ --cx-project="${CI_PROJECT_NAME}" \ --app="${CI_PROJECT_NAME}" \ --branch="${CI_COMMIT_REF_NAME}" \ --repo-name="${CI_PROJECT_NAME}" \ --namespace="${CI_PROJECT_NAMESPACE}" \ --merge-id="${CI_MERGE_REQUEST_IID:-}" \ --bug-tracker=GitLabDashboard \ --f="${CI_PROJECT_DIR}" \ --cx-flow.filter-severity=HIGH,CRITICAL \ --incremental="${IS_INCREMENTAL}" # This is what makes results appear in GitLab UI artifacts: when: always # upload report even if job fails reports: sast: gl-sast-report.json # → Pipeline Security tab + MR widget paths: - gl-sast-report.json expire_in: 30 days rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE != "schedule"' allow_failure: false # pipeline goes red on HIGH/CRITICAL findings
--bug-tracker=GitLabDashboard → tells CxFlow to write gl-sast-report.json in GitLab's SAST schemaartifacts.reports.sast → tells GitLab to read that file and render it in the Security tabwhen: always → report uploads even when the job fails so you always see whyallow_failure: false → pipeline fails hard on HIGH/CRITICAL so it blocks merges
Pin the CxFlow Image Digest
Image tags like :latest are mutable — upstream can overwrite them silently. A SHA256 digest is immutable. Run this once on any machine with Docker access.
docker pull checkmarx/cx-flow:latest
docker inspect --format='{{index .RepoDigests 0}}' checkmarx/cx-flow:latestOutput looks like:
checkmarx/cx-flow@sha256:a1b2c3d4e5f6789abc...
Copy the sha256:... part and paste it into your .gitlab-ci.yml:
# Before — unsafe image: checkmarx/cx-flow:latest # After — pinned, reproducible, auditable image: checkmarx/cx-flow@sha256:a1b2c3d4e5f6789abc...
Test the Pipeline
Commit the .gitlab-ci.yml to the repo and push. Then verify everything ran correctly.
1 — Trigger the first scan
# If the file is already in the repo, a push triggers it
git add .gitlab-ci.yml
git commit -m "chore: add Checkmarx SAST pipeline"
git push origin main2 — Watch the pipeline
GitLab → CI/CD → Pipelines → click the running pipeline → checkmarx-sast job
3 — Confirm the report was uploaded
In the pipeline job detail page, look for the artifacts section — gl-sast-report.json should be listed. If it's there, GitLab picked it up.
4 — Confirm scan ran in Checkmarx
Checkmarx Web UI → Projects → look for a project named after your GitLab repo
Checkmarx auto-creates the project on first scan using $CI_PROJECT_NAME.
Where Results Appear in GitLab
Pipeline Security tab — all tiers
CI/CD → Pipelines → <your pipeline> → Security tab
Lists every finding from that scan with severity, CWE, file, and line number.
MR security widget — all tiers
Merge Requests → <your MR> → scroll to Security widget at bottom of page
Shows only new findings introduced by that MR — not pre-existing ones. This is what blocks the merge when allow_failure: false.
Project Security Dashboard — GitLab Ultimate only
Security → Dashboard
Trend view of all findings over time across all scans in the project.
Scan Behavior Reference
| Trigger | Scan type | What gets scanned | Pipeline result on finding |
|---|---|---|---|
Push to main / default branch | Full scan | Entire codebase | Fails on HIGH / CRITICAL |
| Push to any other branch | Incremental | Changed files only — faster | Fails on HIGH / CRITICAL |
| MR opened or updated | Incremental | Changed files only | Fails — blocks merge |
| Scheduled pipeline | Skipped | — | Not triggered by schedule |
Go-Live Checklist
- Checkmarx Manager reachable from GitLab Runner host on port 443
- Checkmarx Scan Engine reachable on port 8088
svc-gitlab-ciaccount created with Scanner role and password-never-expires- Auth test (Step 02 curl) returns an
access_token - GitLab token created with
apiscope only, 90-day expiry - All four group-level CI/CD variables set with correct Protected/Masked flags
- CxFlow image pinned by SHA256 digest — no
:latest .gitlab-ci.ymlcommitted to repo root- Test push made and pipeline visible under CI/CD → Pipelines
gl-sast-report.jsonvisible in pipeline artifacts- Security tab shows findings in GitLab pipeline
- Calendar reminder set to rotate GitLab token in 90 days
Troubleshooting
401 Unauthorized — pipeline fails immediately
The CxFlow container cannot authenticate to Checkmarx. Check in order:
1. CX_USERNAME and CX_PASSWORD variables are set at group level with correct values — typos are common
2. Re-run the Step 02 curl test manually from the runner host to confirm the account itself works
3. The account may be locked after repeated failed attempts — unlock it in Checkmarx User Management
4. Check the CX_SERVER value has no trailing slash
Pipeline runs but Security tab is empty / no report
The scan ran but gl-sast-report.json was not produced or not picked up by GitLab.
1. Confirm --bug-tracker=GitLabDashboard is in your script — this flag is what tells CxFlow to write the GL-schema JSON
2. Check the artifacts section of the job — is gl-sast-report.json listed? If not, CxFlow failed to write it
3. Confirm artifacts.reports.sast: gl-sast-report.json is in the CI file with correct indentation
4. Check CxFlow container logs for errors writing the report file
Scan hangs or times out
Usually a network issue between CxFlow and the Checkmarx Scan Engine.
1. Confirm port 8088 is open between the GitLab Runner network and the Checkmarx Engine host
2. Large repos (>500k LOC) on first scan can take 60–120+ minutes — incremental scans are much faster
3. Check Checkmarx Engine has sufficient CPU/memory — scans queue up and appear hung if the engine is saturated
SSL / TLS errors in CxFlow container
Your Checkmarx instance uses an internal CA cert that the CxFlow JVM doesn't trust.
Fix: build a custom CxFlow image that imports your corporate root CA:
FROM checkmarx/cx-flow@sha256:<pinned-digest>
COPY corp-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates \
&& keytool -import \
-trustcacerts \
-alias corp-root \
-file /usr/local/share/ca-certificates/corp-root.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit \
-nopromptPush this image to your internal registry and use it instead of the public CxFlow image.
Variables not available in pipeline — job fails with empty vars
Variables set as Protected are only available on protected branches. If you're testing on a feature branch:
1. Either temporarily set the variables as unprotected to test
2. Or protect the branch you're testing on: GitLab → Settings → Repository → Protected branches
3. Once confirmed working, re-apply Protected flag and keep Protected branches policy
MR widget not showing security findings
The MR security widget only shows findings from pipelines triggered by the MR event — not from a branch push pipeline.
1. Confirm the pipeline was triggered by CI_PIPELINE_SOURCE == "merge_request_event" — check the pipeline variables in the job detail
2. The MR must be open when the scan completes — results from a closed/merged MR don't retroactively appear
3. GitLab Ultimate is required for the group-level Security Dashboard. The MR widget itself works on all tiers.