# GitLab CI Pipeline for Gitleaks Secret Scanning # Save as: .gitlab-ci.yml or include in existing pipeline # Define stages stages: - security - report # Default Docker image for security jobs image: docker:latest services: - docker:dind variables: # Gitleaks Docker image GITLEAKS_IMAGE: zricethezav/gitleaks:latest # Report output path REPORT_PATH: gitleaks-report.json # SARIF output for GitLab Security Dashboard SARIF_PATH: gl-secret-detection-report.json # Secret scanning job gitleaks-scan: stage: security image: $GITLEAKS_IMAGE # Run on all branches and merge requests rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - if: '$CI_COMMIT_BRANCH =~ /^(develop|release)/' script: # Run Gitleaks scan - echo "Running Gitleaks secret detection..." - | gitleaks detect \ --source . \ --report-path $REPORT_PATH \ --report-format json \ --verbose || true # Convert to GitLab SARIF format for Security Dashboard - | gitleaks detect \ --source . \ --report-path $SARIF_PATH \ --report-format sarif \ --verbose || true # Check if secrets were found - | if [ -s "$REPORT_PATH" ] && [ "$(cat $REPORT_PATH)" != "null" ]; then echo "⚠️ Secrets detected! Review findings below." cat $REPORT_PATH | jq -r '.[] | "File: \(.File)\nLine: \(.StartLine)\nRule: \(.RuleID)\n"' exit 1 else echo "✅ No secrets detected" fi artifacts: paths: - $REPORT_PATH - $SARIF_PATH reports: # GitLab Security Dashboard integration secret_detection: $SARIF_PATH when: always expire_in: 30 days # Allow failure for initial rollout, then set to false allow_failure: false # Optional: Incremental scanning with baseline gitleaks-incremental: stage: security image: $GITLEAKS_IMAGE # Only run on merge requests rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' script: # Download baseline from artifacts or storage - echo "Downloading baseline..." - | if [ -f ".gitleaks-baseline.json" ]; then echo "Using baseline from repository" else echo "No baseline found, running full scan" fi # Run incremental scan - | if [ -f ".gitleaks-baseline.json" ]; then gitleaks detect \ --source . \ --baseline-path .gitleaks-baseline.json \ --report-path new-findings.json \ --report-format json \ --exit-code 1 || true if [ -s "new-findings.json" ] && [ "$(cat new-findings.json)" != "null" ]; then echo "⚠️ New secrets detected since baseline!" cat new-findings.json | jq . exit 1 fi fi artifacts: paths: - new-findings.json when: always expire_in: 7 days # Optional: Create baseline on main branch create-baseline: stage: security image: $GITLEAKS_IMAGE # Only run on main/master branch rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: manual # Manual trigger to avoid overwriting script: - echo "Creating new baseline..." - | gitleaks detect \ --source . \ --report-path .gitleaks-baseline.json \ --report-format json \ --exit-code 0 || true artifacts: paths: - .gitleaks-baseline.json expire_in: 365 days # Optional: Generate human-readable report generate-report: stage: report image: python:3.11-slim dependencies: - gitleaks-scan rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' script: - pip install jinja2 - | python3 << 'EOF' import json import sys from datetime import datetime try: with open('gitleaks-report.json', 'r') as f: findings = json.load(f) if not findings: print("✅ No secrets detected") sys.exit(0) print("# Gitleaks Secret Detection Report") print(f"\n**Generated**: {datetime.now().isoformat()}") print(f"**Total Findings**: {len(findings)}\n") for idx, finding in enumerate(findings, 1): print(f"\n## Finding {idx}") print(f"- **File**: {finding.get('File', 'unknown')}") print(f"- **Line**: {finding.get('StartLine', 'unknown')}") print(f"- **Rule**: {finding.get('RuleID', 'unknown')}") print(f"- **Description**: {finding.get('Description', 'unknown')}") print(f"- **Commit**: {finding.get('Commit', 'N/A')}\n") except FileNotFoundError: print("No report file found") except json.JSONDecodeError: print("No findings in report") EOF artifacts: paths: - gitleaks-report.json # Optional: Comment on merge request comment-mr: stage: report image: alpine:latest dependencies: - gitleaks-scan rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' before_script: - apk add --no-cache curl jq script: - | if [ -s "$REPORT_PATH" ] && [ "$(cat $REPORT_PATH)" != "null" ]; then FINDING_COUNT=$(cat $REPORT_PATH | jq '. | length') COMMENT="## 🔒 Secret Scanning Results\n\n" COMMENT="${COMMENT}⚠️ **${FINDING_COUNT} potential secret(s) detected!**\n\n" COMMENT="${COMMENT}Please review the findings and take immediate action:\n" COMMENT="${COMMENT}1. **Do not merge** this MR until secrets are removed\n" COMMENT="${COMMENT}2. Rotate any exposed credentials immediately\n" COMMENT="${COMMENT}3. Remove secrets from code and use CI/CD variables\n\n" COMMENT="${COMMENT}See pipeline artifacts for detailed findings." # Post comment to merge request curl --request POST \ --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ --data-urlencode "body=$COMMENT" \ "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" fi allow_failure: true # Optional: Scheduled nightly scan nightly-scan: stage: security image: $GITLEAKS_IMAGE # Run on schedule only rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' script: - echo "Running comprehensive nightly secret scan..." - | gitleaks detect \ --source . \ --report-path nightly-scan.json \ --report-format json \ --verbose artifacts: paths: - nightly-scan.json when: always expire_in: 90 days # Send notifications on failure after_script: - | if [ $? -ne 0 ]; then echo "Secrets detected in nightly scan!" # Add notification logic (email, Slack, etc.) fi