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