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.
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.
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?
Because scan execution and gate policy are separate states. The scan jobs preserve evidence; the explicit gate decides whether the findings block delivery.
Why is the fake secret detector custom instead of using a realistic credential?
It demonstrates detection and triage without creating a usable credential or encouraging learners to expose one in Git history/logs.
What is wrong with declaring raw Semgrep JSON as
artifacts:reports:sast?
Raw Semgrep JSON does not automatically conform to GitLab's SAST security-report schema. Preserve it as a normal artifact unless an adapter emits the supported schema.
What additional identity must a container finding include beyond a tag?
The immutable image digest, plus producer source/pipeline and scanner version/database context where relevant.
Which layer should you change when a correct scan is visible but delivery should be blocked?
The policy/gate layer, not the scanner execution layer.
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.
- SAST — official reference.
- SAST analyzers — official reference.
- Secret detection — official reference.
- Secret detection configuration — official reference.
- Container scanning — official reference.
- Dependency scanning — official reference.
- Dependency scanning migration to SBOM — official reference.
- DAST — official reference.
- DAST browser analyzer — official reference.
- IaC scanning — official reference.
- Security configuration — official reference.
- Security policies — official reference.
- Security scanner integration and report schemas — official reference.
- CI/CD YAML syntax — official reference.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.