Chapter 25Lesson 02~220 minutes

SAST, Dependency Scanning, Secret Detection, Container Scanning, DAST, IaC Scanning, and Security Gates: Guided Hands-On Workflow and Core Operations

Run disposable local SAST and synthetic secret scanning, preserve raw reports, map the evidence to GitLab security report concepts, and compare SAST, dependency, container, DAST, and IaC trust boundaries without touching production.

Hands-onSemgrepSynthetic secretsRaw reportsSafe scanning

Learning objectives

  • Run a pinned Semgrep SAST check against synthetic source and preserve raw JSON.
  • Run a deterministic secret detector using an obviously synthetic marker rather than a real credential pattern.
  • Compare SAST, secret, dependency, container, DAST, and IaC input boundaries using concrete evidence.
  • Map local scanner outputs to GitLab-managed SAST/Secret/Container/IaC templates without pretending raw JSON is a GitLab security report.
  • Build an explicit local gate and inspect source, scanner version, findings, report checksums, and gate outcome.

1. Disposable scenario and safety contract

Before scanning, record CI_PIPELINE_SOURCE, ref, CI_COMMIT_SHA, pipeline ID, and job ID so every report can be bound to its producer revision.

Create a disposable project named glci-ch25-security-lab. It contains intentionally insecure synthetic code and an obviously fake marker. No production repository, registry, cloud account, customer URL, credential, or protected environment is required. The mandatory path uses Semgrep OSS plus a tiny deterministic Python detector and normal job artifacts.

Never paste a real secret into this lab to “see if detection works.” Use the custom marker LAB_SECRET_EXAMPLE_NOT_CREDENTIAL. A real leaked credential must be revoked/rotated; deleting it from Git is not remediation.

2. Synthetic source and local rules

# src/app.py -- intentionally vulnerable training code

def calculate(user_expression: str):
    # Synthetic SAST finding: dynamic evaluation of untrusted text.
    return eval(user_expression)

LAB_MARKER = "LAB_SECRET_EXAMPLE_NOT_CREDENTIAL"
# security/semgrep-rules.yml
rules:
  - id: chapter25-python-eval
    languages: [python]
    message: Avoid dynamic eval of user-controlled expressions.
    severity: ERROR
    pattern: eval(...)

The Semgrep rule is local, reviewable, and deterministic. That makes the teaching result independent of a cloud ruleset changing overnight. Production programs can use managed rules, but must record the rule source/version used for each scan.

3. Deterministic synthetic-secret detector

# tools/secret_scan.py
from pathlib import Path
import hashlib, json, re

PATTERN = re.compile(r"LAB_SECRET_[A-Z0-9_]+")
findings = []
for path in Path("src").rglob("*.py"):
    text = path.read_text(encoding="utf-8")
    for line_no, line in enumerate(text.splitlines(), 1):
        for match in PATTERN.finditer(line):
            value = match.group(0)
            findings.append({
                "id": hashlib.sha256(f"{path}:{line_no}:{value}".encode()).hexdigest()[:16],
                "path": str(path),
                "line": line_no,
                "kind": "synthetic_training_marker",
                "value_sha256": hashlib.sha256(value.encode()).hexdigest(),
            })
report = {"scanner":"chapter25-synthetic-secret","version":"1.0", "findings":findings}
Path("security-reports").mkdir(exist_ok=True)
Path("security-reports/secrets.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"synthetic_secret_findings={len(findings)}")

4. Progressive pipeline: scan first, gate separately

stages: [scan, gate]

sast_semgrep:
  stage: scan
  image: semgrep/semgrep:1.176.0
  script:
    - mkdir -p security-reports
    - semgrep --version | tee security-reports/semgrep-version.txt
    - semgrep scan --config security/semgrep-rules.yml --json --output security-reports/semgrep.json src/
    - sha256sum security-reports/* | tee security-reports/semgrep-SHA256SUMS
  artifacts:
    when: always
    expire_in: 7 days
    paths: [security-reports/]

secret_synthetic:
  stage: scan
  image: python:3.12-alpine
  script:
    - python tools/secret_scan.py
    - python -m json.tool security-reports/secrets.json >/dev/null
    - sha256sum security-reports/secrets.json > security-reports/secrets.sha256
  artifacts:
    when: always
    expire_in: 7 days
    paths: [security-reports/]

security_gate:
  stage: gate
  image: python:3.12-alpine
  needs:
    - job: sast_semgrep
      artifacts: true
    - job: secret_synthetic
      artifacts: true
  script:
    - python tools/security_gate.py

Both scan jobs are evidence producers. The gate is a separate policy decision. That separation keeps “scanner execution completed” distinct from “delivery is allowed.” It also lets a future policy change thresholds without rewriting the scanner itself.

5. Gate the evidence transparently

# tools/security_gate.py
from pathlib import Path
import json, sys

semgrep = json.loads(Path("security-reports/semgrep.json").read_text())
secrets = json.loads(Path("security-reports/secrets.json").read_text())
sast_count = len(semgrep.get("results", []))
secret_count = len(secrets.get("findings", []))
print(f"sast_findings={sast_count} synthetic_secret_findings={secret_count}")
# First run deliberately blocks so learners must triage/remediate.
sys.exit(1 if (sast_count + secret_count) > 0 else 0)

Do not implement a gate by grepping human-formatted console text. Parse a stable machine-readable structure, record the rule/threshold, and fail explicitly. The checkpoint later adds a narrow exception record for the synthetic marker so exceptions remain reviewable rather than invisible.

6. Map to GitLab-managed Free scanners without changing the mandatory lab

On a compatible GitLab runner, current docs show the following stable templates for GitLab-managed basic scanning. Validate them in a disposable project first. Their analyzers emit GitLab security-report schemas; your local Semgrep/raw secret JSON above does not.

include:
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Jobs/Container-Scanning.gitlab-ci.yml
  - template: Jobs/SAST-IaC.gitlab-ci.yml

variables:
  # Current recommended switch when you intentionally want supported AST jobs
  # in merge-request pipelines.
  AST_ENABLE_MR_PIPELINES: "true"

Container scanning also needs an image target, normally identified by a registry path and preferably an immutable digest. Do not give scanners external-registry write credentials. Scan credentials should be read-only and narrowly scoped.

7. Compare the six scanner boundaries

Class Disposable exercise Evidence Do not infer
SAST Semgrep on src/ Raw Semgrep JSON + tool version + source SHA That dependencies or runtime are safe.
Secret Custom fake-marker detector Hashed marker evidence + source location That no real secret was ever leaked elsewhere.
Dependency Local lockfile/SBOM inspection or package-audit tool Resolved component/version list + advisory result That the dependency is reachable or the app is exploitable.
Container Optional Trivy/GitLab scan of a disposable image by digest Image digest + scanner DB/tool version + report That the running deployment matches the image or is safely configured.
DAST Faithful simulation or a disposable local test service only Exact test URL/build + scan profile + report That production may be scanned safely.
IaC GitLab IaC scan or local rules against synthetic Terraform/K8s IaC file SHA + KICS/other rule identity + report That deployed cloud state equals repository state.

8. DAST safety: target identity is part of the configuration

A DAST job is not “just another scanner.” It sends requests to a running target. The target URL, environment identity, allowed hostnames, and scan mode must therefore be reviewed as security-sensitive configuration. GitLab's current DAST documentation explicitly says to run DAST against a test server. For this course's mandatory path, do not send active security traffic outside a local/disposable target.

9. Challenge: choose the correct layer

The Semgrep report contains one ERROR finding, but the scan job is green and the gate is missing. Which layer is wrong? The scanner did its job; the problem is the policy/gate layer. Add an explicit gate that consumes preserved report evidence. Do not “fix” the scanner by making it crash on every finding unless that is the documented organization contract.

10. Cleanup

Remove only the disposable project/branch after preserving the requested evidence. No registry, cloud, or production target should exist for the mandatory lab. If an optional container image was pushed, delete only its exact lab tag/digest according to your registry policy.

Knowledge check

Why are the scan jobs allowed to succeed even when findings exist?

Why is the fake secret detector custom instead of using a realistic credential?

What is wrong with declaring raw Semgrep JSON as artifacts:reports:sast?

What additional identity must a container finding include beyond a tag?

Which layer should you change when a correct scan is visible but delivery should be blocked?

Next lesson

Configuration, design choices, and tradeoffs

Choose scanner ownership, template/component update policy, advisory versus blocking behavior, and exception workflow without creating hidden bypasses.

Version and compatibility note

GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.

Official references and version notes

Documentation verification date: 2026-09-12. Basic GitLab SAST, pipeline Secret Detection, Container Scanning, and IaC scanning are documented for Free/Premium/Ultimate. Current GitLab Dependency Scanning and DAST are Ultimate. Security policy enforcement is Ultimate. GitLab's current Dependency Scanning direction is SBOM-based; the legacy Gemnasium-based implementation was deprecated in GitLab 17.9 and is proposed for removal in GitLab 20.0. Stable security templates are recommended for production workflows; Latest templates change more frequently. AST_ENABLE_MR_PIPELINES="true" is the current recommended switch for supported application-security scans in merge-request pipelines. Semgrep 1.176.0 was released on 2026-09-01 and is used here only as a pinned OSS teaching tool. Revalidate external tool versions and image provenance before production use.

Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.