Implementing Multi-Stage Security Gates
Implementing Multi-Stage Security Gates
Modern CI/CD pipelines benefit from security gates at multiple stages, each focused on specific security aspects. Pre-commit gates catch obvious issues before they enter version control. Build-time gates validate code security and dependencies. Pre-deployment gates ensure infrastructure and configuration security. Runtime gates monitor deployed applications for security compliance.
# Comprehensive security gate implementation in GitLab CI
stages:
- validate
- build
- security-scan
- security-gate
- deploy
variables:
SECURITY_GATE_ENABLED: "true"
CRITICAL_THRESHOLD: "0"
HIGH_THRESHOLD: "5"
MEDIUM_THRESHOLD: "20"
# Pre-commit validation gate
pre-commit-checks:
stage: validate
script:
# Check for secrets
- |
echo "Scanning for secrets..."
trufflehog filesystem . --json > secrets-scan.json
if [ -s secrets-scan.json ]; then
echo "FAILED: Secrets detected in code!"
cat secrets-scan.json
exit 1
fi
# Validate commit messages
- |
echo "Validating commit messages..."
gitlint --commits origin/main..HEAD
# Check file permissions
- |
echo "Checking file permissions..."
find . -type f -executable | grep -E '\.(yaml|yml|json|xml)$' && {
echo "FAILED: Configuration files should not be executable"
exit 1
}
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# Security scanning aggregation
security-scan-aggregate:
stage: security-scan
needs:
- job: sast
- job: dependency-check
- job: container-scan
- job: iac-scan
script:
- |
# Aggregate all security findings
python3 - <<'EOF'
import json
import sys
def load_json_file(filename):
try:
with open(filename, 'r') as f:
return json.load(f)
except:
return {}
# Load all scan results
sast_results = load_json_file('gl-sast-report.json')
dep_results = load_json_file('gl-dependency-scanning-report.json')
container_results = load_json_file('gl-container-scanning-report.json')
iac_results = load_json_file('gl-iac-scanning-report.json')
# Aggregate vulnerabilities by severity
aggregated = {
'critical': [],
'high': [],
'medium': [],
'low': [],
'info': []
}
# Process each report
for report in [sast_results, dep_results, container_results, iac_results]:
vulnerabilities = report.get('vulnerabilities', [])
for vuln in vulnerabilities:
severity = vuln.get('severity', 'info').lower()
aggregated[severity].append({
'scanner': vuln.get('scanner', {}).get('name', 'unknown'),
'location': vuln.get('location', {}),
'message': vuln.get('message', ''),
'solution': vuln.get('solution', ''),
'identifiers': vuln.get('identifiers', [])
})
# Save aggregated report
with open('security-summary.json', 'w') as f:
json.dump(aggregated, f, indent=2)
# Print summary
print("Security Scan Summary:")
print(f"Critical: {len(aggregated['critical'])}")
print(f"High: {len(aggregated['high'])}")
print(f"Medium: {len(aggregated['medium'])}")
print(f"Low: {len(aggregated['low'])}")
EOF
artifacts:
paths:
- security-summary.json
reports:
security: security-summary.json
# Security gate decision
security-gate-check:
stage: security-gate
script:
- |
# Load security summary
python3 - <<'EOF'
import json
import os
import sys
# Load thresholds from environment
critical_threshold = int(os.getenv('CRITICAL_THRESHOLD', '0'))
high_threshold = int(os.getenv('HIGH_THRESHOLD', '5'))
medium_threshold = int(os.getenv('MEDIUM_THRESHOLD', '20'))
# Load scan results
with open('security-summary.json', 'r') as f:
results = json.load(f)
critical_count = len(results.get('critical', []))
high_count = len(results.get('high', []))
medium_count = len(results.get('medium', []))
# Check against thresholds
failed = False
failure_reasons = []
if critical_count > critical_threshold:
failed = True
failure_reasons.append(f"Critical vulnerabilities ({critical_count}) exceed threshold ({critical_threshold})")
if high_count > high_threshold:
failed = True
failure_reasons.append(f"High vulnerabilities ({high_count}) exceed threshold ({high_threshold})")
if medium_count > medium_threshold:
failed = True
failure_reasons.append(f"Medium vulnerabilities ({medium_count}) exceed threshold ({medium_threshold})")
# Generate gate decision report
gate_decision = {
'passed': not failed,
'timestamp': os.popen('date -u +"%Y-%m-%dT%H:%M:%SZ"').read().strip(),
'counts': {
'critical': critical_count,
'high': high_count,
'medium': medium_count
},
'thresholds': {
'critical': critical_threshold,
'high': high_threshold,
'medium': medium_threshold
},
'failure_reasons': failure_reasons
}
with open('gate-decision.json', 'w') as f:
json.dump(gate_decision, f, indent=2)
# Exit with appropriate code
if failed:
print("SECURITY GATE FAILED:")
for reason in failure_reasons:
print(f" - {reason}")
sys.exit(1)
else:
print("Security gate passed!")
EOF
artifacts:
paths:
- gate-decision.json
rules:
- if: '$SECURITY_GATE_ENABLED == "true"'
# Exception handling workflow
request-security-exception:
stage: security-gate
when: on_failure
script:
- |
# Generate exception request
python3 scripts/generate_exception_request.py \
--scan-results security-summary.json \
--project "$CI_PROJECT_NAME" \
--branch "$CI_COMMIT_REF_NAME" \
--requester "$GITLAB_USER_EMAIL"
# Create issue for security team review
- |
curl --request POST \
--header "PRIVATE-TOKEN: $SECURITY_TEAM_TOKEN" \
--data @exception-request.json \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/issues"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: on_failure