my blog

$ cat gitlab-ci-cd/part-01.md

GitLab CI/CD : Container-Native Pipelines with Built-in SAST

Introduction: Why I Switched from Jenkins to GitLab

After building Jenkins pipelines, I was comfortable with Jenkins. Then I discovered GitLab CI/CD and everything changed.

The fundamental difference? GitLab is container-native from day one.

Jenkins approach:

  • Install Jenkins on server
  • Configure agents with Java, Maven, Node.js, Python
  • Install plugins for Docker, SonarQube, JUnit
  • Write Groovy pipeline code

GitLab approach:

  • Install GitLab Runner
  • Install Docker
  • Write YAML
  • Everything else runs in containers

This article shows my complete migration with deep technical explanations of how GitLab’s container-native architecture works internally.

GitLab CI/CD Architecture: How It Actually Works

The Component Stack

┌─────────────────────────────────────┐
│         GitLab Server               │
│  (Manages jobs, stores artifacts)   │
└──────────────┬──────────────────────┘
               │
               │ HTTPS/gRPC
               │
┌──────────────▼──────────────────────┐
│       GitLab Runner                 │
│  (Executes jobs, pulls containers)  │
└──────────────┬──────────────────────┘
               │
        ┌──────┴──────┐
        │             │
┌───────▼─────┐ ┌─────▼──────┐
│  Docker     │ │   Shell    │
│  Executor   │ │  Executor  │
└─────────────┘ └────────────┘

Communication flow:

  1. GitLab Server (gitlab.com or self-hosted):
  2. GitLab Runner (your machine/VM):
  3. Executors:

My First GitLab Pipeline: Deep Technical Analysis

Repository: GitLab/java-project/java-project.yml

stages:
- maven-test-stage
- maven-deploy-stage
- sonarqube-stage
- docker-build-stage
- docker-push-stage
- docker-run-stage
- aws-configuration-stage
- kubernetes-stage

maven-test-job:
  stage: maven-test-stage
  tags:
  - macos
  - personal
  - shell
  before_script:
  - mvn -v
  script:
  - mvn clean package
  - mvn test
  artifacts:
    name: war
    paths:
    - "target/my-webapp.war"
    reports:
      junit:
      - target/surefire-reports/TEST.*.xml
    expire_in: 30 days

Deep Dive: Tags and Runner Selection

tags:
- macos
- personal
- shell

How runner selection works:

Step 1: Job enters queue

When pipeline starts, GitLab creates job with these requirements:

{
  "job_id": 12345,
  "tags_required": ["macos", "personal", "shell"],
  "image": null,
  "services": []
}

Step 2: Runners check eligibility

Every 3 seconds, each runner asks GitLab: “Do you have jobs for me?”

Runner A (Linux, tags: [linux, docker]):
- Has tag "macos"? NO
- Skip this job

Runner B (macOS, tags: [macos, personal, shell]):
- Has tag "macos"? YES
- Has tag "personal"? YES
- Has tag "shell"? YES
- Accept this job! ✅

Step 3: Runner claims job

POST https://gitlab.com/api/v4/jobs/12345/claim
Headers:
  Authorization: Bearer runner-token-xyz

Response:
{
  "status": "claimed",
  "runner_id": 789
}

Step 4: Runner executes job

On macOS runner:

# GitLab Runner executes these commands directly on macOS
cd /Users/runner/builds/project
mvn -v
mvn clean package
mvn test

Why this matters:

# Jenkins approach
pipeline {
    agent { label 'macos-agent' }
}

Single label, finds first matching agent.

# GitLab approach
tags:
- macos
- personal  
- shell

Must match ALL tags. More specific control.

Deep Dive: Artifacts System

artifacts:
  name: war
  paths:
  - "target/my-webapp.war"
  reports:
    junit:
    - target/surefire-reports/TEST.*.xml
  expire_in: 30 days

What happens during artifact upload:

Step 1: Job completes successfully

GitLab Runner scans for artifacts:

# Runner finds these files
/builds/project/target/my-webapp.war
/builds/project/target/surefire-reports/TEST.UserServiceTest.xml
/builds/project/target/surefire-reports/TEST.AppTest.xml

Step 2: Create ZIP archive

# Runner creates temporary ZIP
cd /builds/project
zip -r /tmp/artifacts-12345.zip target/my-webapp.war target/surefire-reports/TEST.*.xml

# ZIP structure:
artifacts-12345.zip
├── target/
│   ├── my-webapp.war
│   └── surefire-reports/
│       ├── TEST.UserServiceTest.xml
│       └── TEST.AppTest.xml

Step 3: Upload to GitLab

POST https://gitlab.com/api/v4/jobs/12345/artifacts
Headers:
  Content-Type: multipart/form-data
  JOB-TOKEN: secret-job-token
Body:
  file: /tmp/artifacts-12345.zip
  artifact_type: archive
  expire_in: 2592000  (30 days in seconds)

Response:
{
  "artifacts_file": {
    "filename": "artifacts.zip",
    "size": 5242880  (5 MB)
  },
  "artifacts_expire_at": "2026-01-19T10:30:00Z"
}

Step 4: Parse JUnit reports

GitLab extracts and parses JUnit XML:

<!-- TEST.UserServiceTest.xml -->
<testsuite tests="3" failures="0" errors="0" time="0.532">
  <testcase name="testUserCreation" time="0.123"/>
  <testcase name="testUserUpdate" time="0.204"/>
  <testcase name="testUserDeletion" time="0.205"/>
</testsuite>

Stores in database:

INSERT INTO ci_job_artifacts (
  job_id,
  file_type,
  size,
  expire_at,
  file_store
) VALUES (
  12345,
  'junit',
  5242880,
  '2026-01-19 10:30:00',
  1  # local storage
);

Step 5: Display in UI

GitLab UI shows:

Tests: 3 passed, 0 failed
Duration: 0.532s
Coverage: N/A

Artifact retrieval by other jobs:

deploy-job:
  stage: deploy
  dependencies:
  - maven-test-job
  script:
  - ls target/my-webapp.war  # File is here!

What happens:

  1. deploy-job starts
  2. GitLab downloads artifacts-12345.zip
  3. Extracts to job workspace
  4. Job can access target/my-webapp.war

Artifact expiration:

After 30 days, GitLab cron job runs:

# GitLab background job (simplified)
ArtifactExpireWorker.perform_async

def perform
  expired = CiJobArtifact.where('expire_at < ?', Time.now)
  expired.find_each do |artifact|
    artifact.file.remove!  # Delete from storage
    artifact.destroy       # Delete database record
  end
end

Deep Dive: SonarQube Integration

sonarqube-check:
  stage: sonarqube-stage
  image: maven:3.6.3-jdk-11
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths:
    - .sonar/cache
  script:
  - mvn verify sonar:sonar -Dsonar.projectKey=root_test_AZOKVHhuL2aw2rzHZn9m
  allow_failure: true
  only:
  - main

Technical breakdown:

1. Variable SONAR_USER_HOME:

SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"

What this does:

Without this:

SonarQube stores cache in: /root/.sonar/
Next job runs: Fresh container, /root/.sonar/ is empty
Result: Re-downloads all analysis plugins (300MB)

With this:

SonarQube stores cache in: /builds/project/.sonar/
GitLab caches this directory
Next job runs: Cache restored to /builds/project/.sonar/
Result: Uses cached plugins (0MB download)

Time savings:

  • First run: 45 seconds to download SonarQube plugins
  • Cached run: 2 seconds to extract cache
  • 43 seconds saved per run!

2. Variable GIT_DEPTH:

GIT_DEPTH: "0"

Default GitLab behavior (GIT_DEPTH=50):

git clone --depth=50 https://gitlab.com/user/project.git

Downloads only last 50 commits:

Commit history:
[Latest] abc123 (downloaded)
[Latest-1] def456 (downloaded)
...
[Latest-49] xyz789 (downloaded)
[Latest-50] old999 (downloaded)
[Latest-51] ancient (NOT downloaded)

Why this breaks SonarQube:

SonarQube analyzes:

  • Code evolution over time
  • When bugs were introduced
  • Technical debt trends

Needs full Git history!

With GIT_DEPTH=0:

git clone --depth=0 https://gitlab.com/user/project.git
# Same as: git clone https://gitlab.com/user/project.git

Downloads ENTIRE history:

All commits from first to latest
SonarQube can analyze properly

Trade-off:

  • GIT_DEPTH=50: Fast clone (5 seconds), incomplete analysis
  • GIT_DEPTH=0: Slow clone (30 seconds), complete analysis

3. Cache mechanism deep dive:

cache:
  key: "${CI_JOB_NAME}"
  paths:
  - .sonar/cache

How caching works:

First run (cache miss):

Step 1: Job starts, no cache found
Step 2: SonarQube downloads plugins to .sonar/cache/
Step 3: Analysis completes
Step 4: GitLab Runner creates cache archive:
  tar -czf cache-sonarqube-check.tar.gz .sonar/cache/
Step 5: Upload cache to GitLab server:
  PUT /api/v4/projects/123/cache?key=sonarqube-check
  Body: cache-sonarqube-check.tar.gz (300MB)

Second run (cache hit):

Step 1: Job starts
Step 2: Runner checks GitLab server:
  GET /api/v4/projects/123/cache?key=sonarqube-check
  Response: cache-sonarqube-check.tar.gz exists
Step 3: Download and extract:
  curl -o cache.tar.gz https://gitlab.com/.../cache
  tar -xzf cache.tar.gz
Step 4: .sonar/cache/ directory restored
Step 5: SonarQube uses cached plugins

Cache key strategies:

# Per-job caching
cache:
  key: "${CI_JOB_NAME}"  # Key: "sonarqube-check"

# Per-branch caching
cache:
  key: "${CI_COMMIT_REF_SLUG}"  # Key: "main" or "feature-123"

# Per-branch-per-job caching
cache:
  key: "${CI_COMMIT_REF_SLUG}-${CI_JOB_NAME}"  # Key: "main-sonarqube-check"

My choice: ${CI_JOB_NAME} because SonarQube cache is same across all branches.

4. allow_failure behavior:

allow_failure: true

What happens when job fails:

Without allow_failure:

Job fails → Pipeline status: FAILED ❌
Blocks deployment
Manual intervention required

With allow_failure:

Job fails → Pipeline status: SUCCESS (with warnings) ⚠️
Deployment continues
Quality issues logged but don't block

My use case:

SonarQube sometimes fails on:

  • Minor code smells
  • Slight decrease in coverage
  • Acceptable technical debt

I want to see the results but not block deployment.

Job status combinations:

Job Result allow_failure Pipeline Status Success false Success ✅ Success true Success ✅ Failure false Failed ❌ Failure true Success (with warnings) ⚠️

Container Registry: Technical Deep Dive

docker-job:
  stage: docker-build-stage
  image: docker:20.10.16
  services:
  - docker:20.10.16-dind
  before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com
  script:
  - docker build -t registry.gitlab.com/lowyiiii/java-project .
  - docker push registry.gitlab.com/lowyiiii/java-project

Docker-in-Docker (DinD) Architecture

What actually happens when job runs:

Step 1: Runner starts job container

# GitLab Runner executes:
docker run \
  --name job-container \
  --network gitlab-runner \
  docker:20.10.16 \
  sh -c "docker login ... && docker build ..."

Step 2: Runner starts DinD service container

# Simultaneously, Runner executes:
docker run \
  --name dind-service \
  --privileged \
  --network gitlab-runner \
  docker:20.10.16-dind

The –privileged flag is critical:

Without –privileged:

  DinD container cannot access host kernel features
  Cannot start Docker daemon
  ERROR: Cannot connect to Docker daemon ❌

With –privileged:

  DinD container gets full host access
  Can start Docker daemon
  Docker commands work ✅

Step 3: Containers communicate

┌─────────────────────────┐
│   job-container         │
│   (docker:20.10.16)     │
│                         │
│   $ docker build ...    │
└───────────┬─────────────┘
            │
            │ TCP connection to
            │ docker:2376 (TLS)
            │
┌───────────▼─────────────┐
│   dind-service          │
│   (docker:20.10.16-dind)│
│                         │
│   Docker Daemon         │
│   listening on :2376    │
└─────────────────────────┘

Environment variables automatically set:

# Inside job container:
echo $DOCKER_HOST
# Output: tcp://docker:2376

echo $DOCKER_TLS_VERIFY
# Output: 1

echo $DOCKER_CERT_PATH
# Output: /certs/client

This tells Docker client to connect to DinD daemon!

Step 4: Docker build process

# In job container:
docker build -t registry.gitlab.com/lowyiiii/java-project .

# What happens:
1. Docker client sends build context to DinD daemon
2. DinD daemon builds image inside its own container
3. Image stored in DinD daemon's storage
4. Image layers never touch job container

Storage architecture:

Host machine:
/var/lib/docker/
├── containers/
│   ├── dind-service/
│   └── job-container/
└── volumes/
    └── dind-storage/
        └── overlay2/
            └── [image layers stored here]

Step 5: Docker push

docker push registry.gitlab.com/lowyiiii/java-project

# Process:
1. DinD daemon reads image layers from its storage
2. Chunks layers into 10MB blobs
3. Compresses with gzip
4. Uploads to registry.gitlab.com via HTTPS
5. Sends manifest listing all layers

Network flow:

job-container → dind-service → internet → registry.gitlab.com
   (client)      (daemon)                    (registry)

GitLab Container Registry Authentication

before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com

GitLab provides these variables automatically:

# Job environment:
CI_REGISTRY_USER="gitlab-ci-token"
CI_REGISTRY_PASSWORD="glcbt-Xyj4k9m..."  # Job token (changes per job!)
CI_REGISTRY="registry.gitlab.com"
CI_REGISTRY_IMAGE="registry.gitlab.com/lowyiiii/java-project"

How job token authentication works:

Step 1: Job starts, GitLab generates token

# GitLab server (simplified)
def create_job_token(job)
  token = SecureRandom.base64(32)
  
  JobToken.create!(
    job_id: job.id,
    token: token,
    expires_at: job.finished_at + 1.hour
  )
  
  token
end

Step 2: Docker login request

POST https://registry.gitlab.com/jwt/auth
Headers:
  Authorization: Basic Z2l0bGFiLWNpLXRva2VuOmdsY2J0LVh5ajRrOW0uLi4=
  # Base64("gitlab-ci-token:glcbt-Xyj4k9m...")

Response:
{
  "token": "eyJhbGciOiJSUzI1NiIs...",  # JWT
  "expires_in": 300,
  "issued_at": "2024-12-19T10:00:00Z"
}

Step 3: Docker saves token

# Docker creates ~/.docker/config.json
{
  "auths": {
    "registry.gitlab.com": {
      "auth": "Z2l0bGFiLWNpLXRva2VuOmdsY2J0LVh5ajRrOW0uLi4="
    }
  }
}

Step 4: Docker push with JWT

PUT https://registry.gitlab.com/v2/lowyiiii/java-project/blobs/uploads/
Headers:
  Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Body:
  [layer blob data]

GitLab verifies JWT:
1. Decodes JWT
2. Checks signature
3. Validates expiration
4. Checks job permissions
5. Allows upload ✅

Why job tokens are secure:

Traditional password:

  • Same password for all jobs
  • If leaked, all projects compromised
  • Requires manual rotation

Job token:

  • Unique per job
  • Expires 1 hour after job completion
  • Automatically rotated
  • Limited to specific project
  • Job-specific permissions

AWS EKS Deployment: kubectl Installation Deep Dive

deploy-job:
  stage: deploy
  image:
    name: amazon/aws-cli
    entrypoint: [ "" ]
  before_script:
  - curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.30.2/2024-07-12/bin/linux/amd64/kubectl
  - curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.30.2/2024-07-12/bin/linux/amd64/kubectl.sha256
  - sha256sum -c kubectl.sha256
  - chmod +x ./kubectl
  - mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$HOME/bin:$PATH
  script:
  - aws configure set aws_access_key $AWS_ACCESS_KEY_ID
  - kubectl config use-context "$KUBE_CONTEXT"
  - kubectl apply -f Application.yaml

Why Entrypoint Override is Necessary

Default amazon/aws-cli Dockerfile:

FROM python:3.11-slim
RUN pip install awscli
ENTRYPOINT ["aws"]
CMD ["--help"]

Without entrypoint override:

deploy-job:
  image: amazon/aws-cli
  script:
  - echo "Starting deployment"

What GitLab Runner actually executes:

docker run amazon/aws-cli sh -c "echo 'Starting deployment'"

# Docker combines:
# ENTRYPOINT ["aws"] + COMMAND ["sh", "-c", "echo 'Starting deployment'"]
# Result: aws sh -c "echo 'Starting deployment'"
# ERROR: Invalid AWS command! ❌

With entrypoint override:

deploy-job:
  image:
    name: amazon/aws-cli
    entrypoint: [ "" ]
  script:
  - echo "Starting deployment"

What GitLab Runner executes:

docker run --entrypoint="" amazon/aws-cli sh -c "echo 'Starting deployment'"

# Docker uses:
# ENTRYPOINT [] (empty)
# COMMAND ["sh", "-c", "echo 'Starting deployment'"]
# Result: sh -c "echo 'Starting deployment'"
# SUCCESS: Starting deployment ✅

kubectl Binary Verification Process

curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.30.2/2024-07-12/bin/linux/amd64/kubectl
curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.30.2/2024-07-12/bin/linux/amd64/kubectl.sha256
sha256sum -c kubectl.sha256

What happens step-by-step:

Step 1: Download kubectl binary

curl -O https://s3.../kubectl

# Downloads:
-rwxr-xr-x 1 root root 48234567 Dec 19 10:00 kubectl

Step 2: Download checksum file

curl -O https://s3.../kubectl.sha256

# Downloads:
cat kubectl.sha256
# Output:
7d9e8c1f4e5a6b3c2d1a0f9e8d7c6b5a4e3d2c1b0a9f8e7d6c5b4a3e2d1c0f9e  kubectl

Step 3: Verify checksum

sha256sum -c kubectl.sha256

# sha256sum process:
1. Reads kubectl.sha256 file
2. Extracts expected hash: 7d9e8c1f4e5a6b3c...
3. Calculates actual hash of kubectl binary:
   sha256sum kubectl
   # Calculated: 7d9e8c1f4e5a6b3c...
4. Compares:
   if expected == calculated:
       print "kubectl: OK"
       exit 0
   else:
       print "kubectl: FAILED"
       exit 1

Why this matters:

Scenario 1: Compromised download

# Attacker replaces kubectl with malicious version
# Malicious kubectl hash: abc123...
# AWS official hash: 7d9e8c...

sha256sum -c kubectl.sha256
# kubectl: FAILED
# sha256sum: WARNING: 1 computed checksum did NOT match
# Pipeline fails ✅ Attack prevented!

Scenario 2: Corrupted download

# Network error causes partial download
# Partial kubectl hash: def456...
# Expected hash: 7d9e8c...

sha256sum -c kubectl.sha256
# kubectl: FAILED
# Pipeline fails ✅ Corruption detected!

Step 4: Make kubectl executable and add to PATH

chmod +x ./kubectl
# Changes permissions:
# Before: -rw-r--r-- kubectl
# After:  -rwxr-xr-x kubectl

mkdir -p $HOME/bin
# Creates: /root/bin/

cp ./kubectl $HOME/bin/kubectl
# Copies kubectl to: /root/bin/kubectl

export PATH=$HOME/bin:$PATH
# Updates PATH:
# Before: /usr/local/bin:/usr/bin:/bin
# After:  /root/bin:/usr/local/bin:/usr/bin:/bin

# Now kubectl is available:
which kubectl
# Output: /root/bin/kubectl ✅

Kubernetes Context Management

kubectl config use-context "$KUBE_CONTEXT"

What is KUBE_CONTEXT?

GitLab provides this variable from project settings:

Settings → CI/CD → Variables:
KUBE_CONTEXT = "lowyiiii/java-project:my-agent"

Kubeconfig structure:

# ~/.kube/config
apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUd...
    server: https://abc123.gr7.us-east-1.eks.amazonaws.com
  name: my-eks-cluster
contexts:
- context:
    cluster: my-eks-cluster
    namespace: production
    user: gitlab-deploy
  name: lowyiiii/java-project:my-agent
users:
- name: gitlab-deploy
  user:
    token: k8s-aws-v1.aHR0cHM6...

What kubectl config use-context does:

kubectl config use-context "lowyiiii/java-project:my-agent"

# Modifies ~/.kube/config:
current-context: lowyiiii/java-project:my-agent

# Now kubectl uses:
# - Cluster: my-eks-cluster
# - Namespace: production
# - User: gitlab-deploy

Verification:

kubectl config current-context
# Output: lowyiiii/java-project:my-agent

kubectl cluster-info
# Output:
# Kubernetes control plane is running at https://abc123.gr7.us-east-1.eks.amazonaws.com
# CoreDNS is running at https://abc123.gr7.us-east-1.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Built-in SAST: How It Works Internally

sast:
  stage: test
include:
- template: Jobs/SAST.gitlab-ci.yml

This ONE line enables security scanning!

What Actually Happens

Step 1: Template inclusion

include:
- template: Jobs/SAST.gitlab-ci.yml

GitLab fetches from internal repository:

https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml

Template contents (simplified):

# GitLab's SAST template
variables:
  SAST_EXCLUDED_PATHS: "spec,test,tests,tmp"
  SAST_EXCLUDED_ANALYZERS: ""

.sast-analyzer:
  stage: test
  allow_failure: true
  artifacts:
    reports:
      sast: gl-sast-report.json

spotbugs-sast:
  extends: .sast-analyzer
  image: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
  rules:
  - if: $SAST_DISABLED
    when: never
  - if: $CI_COMMIT_BRANCH
    exists:
    - "**/*.java"
    - "**/pom.xml"

semgrep-sast:
  extends: .sast-analyzer
  image: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG"
  rules:
  - if: $SAST_DISABLED
    when: never
  - if: $CI_COMMIT_BRANCH

Step 2: GitLab detects language

# GitLab scans repository:
find . -name "*.java"
# Found: src/main/java/com/example/App.java

find . -name "pom.xml"
# Found: pom.xml

# Conclusion: Java project detected!
# Runs: spotbugs-sast job

Step 3: Spotbugs analyzer runs

# GitLab starts container:
docker run \
  -v /builds/project:/code \
  registry.gitlab.com/gitlab-org/security-products/analyzers/spotbugs:latest \
  /analyzer run

# Inside container:
cd /code
mvn compile  # Compile Java code
spotbugs -textui -xml -output gl-sast-report.json target/classes/

# Spotbugs scans for:
# - SQL injection
# - XXE (XML External Entity)
# - Path traversal
# - Insecure randomness
# - Hard-coded credentials

Step 4: Generate SAST report

// gl-sast-report.json
{
  "version": "14.0.0",
  "vulnerabilities": [
    {
      "id": "SQL_INJECTION_HIBERNATE",
      "category": "sast",
      "name": "SQL Injection",
      "message": "Potential SQL injection",
      "description": "User input is used in SQL query without sanitization",
      "severity": "High",
      "scanner": {
        "id": "spotbugs",
        "name": "SpotBugs"
      },
      "location": {
        "file": "src/main/java/com/example/UserService.java",
        "start_line": 45,
        "end_line": 47
      },
      "identifiers": [
        {
          "type": "spotbugs_type",
          "name": "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE",
          "value": "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE"
        }
      ]
    }
  ]
}

Step 5: GitLab parses and displays

-- GitLab database
INSERT INTO vulnerability_findings (
  project_id,
  scanner_id,
  severity,
  confidence,
  report_type,
  primary_identifier_id,
  location_fingerprint,
  raw_metadata
) VALUES (
  123,
  456,
  'high',
  'medium',
  'sast',
  789,
  'abc123...',
  '{"description": "Potential SQL injection", ...}'
);

Step 6: UI display

Security Dashboard → SAST
┌────────────────────────────────────────────┐
│ 1 High Severity Vulnerability Found       │
├────────────────────────────────────────────┤
│ SQL Injection                              │
│ File: UserService.java:45                  │
│ Status: Detected                           │
│ [View Details] [Create Issue] [Dismiss]   │
└────────────────────────────────────────────┘

Merge request integration:

Merge Request !123
┌────────────────────────────────────────────┐
│ ⚠️ Security Report                         │
├────────────────────────────────────────────┤
│ SAST detected 1 new vulnerability:         │
│ • SQL Injection (High) - UserService.java  │
│                                            │
│ [Block Merge] [Approve Override]          │
└────────────────────────────────────────────┘

$ ls -t root-access/ | head -3

Hello world! uncategorized 1 min Building a Job Board API, Part 1: The Foundation job-board-api · 1/1 16 min Part 13 – Kubernetes Autoscaling in Action: A FastAPI Microservice That Scales Itself home-lab · 13/13 6 min

Leave a comment

Your email address will not be published. Required fields are marked *