#!/bin/bash # 26 - CI/CD Pipeline Active Security # Tests: exposed pipeline configs, build artifact leakage, webhook endpoints, # secret scanning in pipeline paths, registry access, branch protection, # OIDC token theft vectors, dependency substitution surface SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../config.sh" OUT_FILE="$OUT/26_cicd_pipeline.txt" echo "=== 26. CI/CD Pipeline Security ===" | tee "$OUT_FILE" echo "Target: $TARGET" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" HOST=$(echo "$TARGET" | sed 's|https\?://||' | cut -d/ -f1) do_test() { local label="$1"; shift local code code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$@") printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" } do_body() { local label="$1"; local pattern="$2"; shift 2 local body code body=$(curl -sk --max-time 8 "$@") code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$@") printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" if [ -n "$pattern" ] && echo "$body" | grep -qiE "$pattern"; then echo " [WARN] Sensitive pattern found: $(echo "$body" | grep -ioE "$pattern" | head -3 | tr '\n' ' ')" | tee -a "$OUT_FILE" fi } # ── 1. CI/CD Config File Exposure ───────────────────────────────────────────── echo "--- CI/CD Config File Exposure ---" | tee -a "$OUT_FILE" CI_PATHS=( "/.github/workflows/deploy.yml" "/.github/workflows/ci.yml" "/.github/workflows/release.yml" "/.github/workflows/main.yml" "/.github/workflows/build.yml" "/.gitlab-ci.yml" "/Jenkinsfile" "/.circleci/config.yml" "/.travis.yml" "/azure-pipelines.yml" "/bitbucket-pipelines.yml" "/.drone.yml" "/Dockerfile" "/docker-compose.yml" "/docker-compose.prod.yml" "/docker-compose.override.yml" "/.dockerignore" "/k8s/" "/kubernetes/" "/helm/" "/charts/" "/deploy/" "/deployment/" "/infrastructure/" "/terraform/" "/ansible/" "/.env.ci" "/.env.build" "/.env.pipeline" "/buildspec.yml" "/cloudbuild.yaml" "/.github/CODEOWNERS" "/.github/dependabot.yml" "/Makefile" "/Taskfile.yml" "/scripts/deploy.sh" "/scripts/build.sh" "/scripts/release.sh" ) for CI_PATH in "${CI_PATHS[@]}"; do code=$(curl -skL -o /dev/null -w "%{http_code}" --max-time 5 -A "$BROWSER_UA" "$TARGET$CI_PATH") if [ "$code" = "200" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then printf ' [%s] %s ← EXPOSED\n' "$code" "$CI_PATH" | tee -a "$OUT_FILE" fi done echo " (CI config scan complete)" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── 2. Build Artifact & Output Exposure ─────────────────────────────────────── echo "--- Build Artifact Exposure ---" | tee -a "$OUT_FILE" ARTIFACT_PATHS=( "/dist/" "/build/" "/out/" "/.next/" "/.nuxt/" "/target/" "/release/" "/artifacts/" "/coverage/" "/lcov-report/" "/test-results/" "/test-output/" "/.nyc_output/" "/junit.xml" "/coverage.xml" "/test-report.xml" "/.cache/" "/node_modules/" "/vendor/" "/__pycache__/" "/.gradle/" "/.m2/" ) for ART in "${ARTIFACT_PATHS[@]}"; do code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 -A "$BROWSER_UA" "$TARGET$ART") if [ "$code" = "200" ] || [ "$code" = "403" ]; then printf ' [%s] %s\n' "$code" "$ART" | tee -a "$OUT_FILE" fi done echo " (artifact scan complete — 403=blocked-but-exists, 200=exposed)" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── 3. Webhook Endpoints ────────────────────────────────────────────────────── echo "--- Webhook / Pipeline Trigger Endpoints ---" | tee -a "$OUT_FILE" WEBHOOK_PATHS=( "/webhook" "/webhooks" "/webhook/github" "/webhook/gitlab" "/webhook/bitbucket" "/webhook/deploy" "/hooks/deploy" "/hooks/push" "/trigger" "/trigger/build" "/api/webhook" "/api/webhooks" "/ci/trigger" "/deploy/trigger" "/build/trigger" "/pipeline/trigger" ) for WH in "${WEBHOOK_PATHS[@]}"; do do_test "Webhook $WH (GET)" -A "$BROWSER_UA" "$TARGET$WH" done echo "" | tee -a "$OUT_FILE" # Forge a fake GitHub webhook delivery echo "--- Webhook Forgery (unsigned GitHub event) ---" | tee -a "$OUT_FILE" FAKE_PAYLOAD="{\"ref\":\"refs/heads/main\",\"repository\":{\"full_name\":\"${PROJECT_NAME}/test\"},\"pusher\":{\"name\":\"attacker\"}}" for WH_PATH in /webhook /webhook/github /api/webhook /hooks/push; do code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 \ -X POST -A "$BROWSER_UA" \ -H 'Content-Type: application/json' \ -H 'X-GitHub-Event: push' \ -H 'X-GitHub-Delivery: aaaaaaaa-0000-0000-0000-000000000000' \ -d "$FAKE_PAYLOAD" "$TARGET$WH_PATH") printf '[%s] Fake GitHub push event → %s\n' "$code" "$WH_PATH" | tee -a "$OUT_FILE" done echo "" | tee -a "$OUT_FILE" # ── 4. GitHub/GitLab API — Branch Protection & Secret Exposure ──────────────── echo "--- GitHub API — Repo & Branch Protection Check ---" | tee -a "$OUT_FILE" GH_REPO="${GITHUB_REPO}" GH_API="https://api.github.com" do_body "Repo visibility & settings" '"private"\|"visibility"' \ -H 'Accept: application/vnd.github+json' \ -A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO" do_body "Branch protection (main)" '"required_status_checks"\|"required_pull_request_reviews"' \ -H 'Accept: application/vnd.github+json' \ -A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO/branches/main/protection" do_body "Public repo list (unauthenticated)" '"full_name"' \ -H 'Accept: application/vnd.github+json' \ -A "pqc-pentest/1.0" "$GH_API/orgs/PQCrypta/repos?type=public&per_page=5" do_body "GitHub Actions secrets (requires auth)" '"secrets"' \ -H 'Accept: application/vnd.github+json' \ -A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO/actions/secrets" do_body "GitHub Actions workflows" '"total_count"' \ -H 'Accept: application/vnd.github+json' \ -A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO/actions/workflows" echo "" | tee -a "$OUT_FILE" # ── 5. OIDC Token Theft Vectors ─────────────────────────────────────────────── echo "--- OIDC / Federation Token Theft Vectors ---" | tee -a "$OUT_FILE" # GitHub Actions OIDC endpoint (only accessible from runner) do_test "GitHub OIDC token endpoint (external probe)" \ "https://pipelines.actions.githubusercontent.com/serviceHosts/token" do_test "GCP Workload Identity Federation probe" \ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@pqcrypta.iam.gserviceaccount.com:generateIdToken" # OIDC token endpoint via potential SSRF in app do_test "OIDC via SSRF ?url=GitHub-Actions-OIDC" \ -A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('https://pipelines.actions.githubusercontent.com/serviceHosts/token'))")" echo "" | tee -a "$OUT_FILE" # ── 6. Dependency Substitution Attack Surface ───────────────────────────────── echo "--- Dependency Substitution / Confusion Attack Surface ---" | tee -a "$OUT_FILE" # Check if internal package names are published on public registries # (namespace squatting / dependency confusion) # Package names to probe for dependency confusion — derived from PROJECT_NAME INTERNAL_PKG_PATTERNS=("${PROJECT_NAME}" "${PROJECT_NAME}-proxy" "${PROJECT_NAME}-api" "${PROJECT_NAME}-collector" "pqc-proxy") for PKG in "${INTERNAL_PKG_PATTERNS[@]}"; do NPM_CODE=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "https://registry.npmjs.org/$PKG") printf ' npm registry: %s → [%s]\n' "$PKG" "$NPM_CODE" | tee -a "$OUT_FILE" done for PKG in "${INTERNAL_PKG_PATTERNS[@]}"; do CARGO_CODE=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "https://crates.io/api/v1/crates/$PKG") printf ' crates.io: %s → [%s]\n' "$PKG" "$CARGO_CODE" | tee -a "$OUT_FILE" done echo "" | tee -a "$OUT_FILE" # ── 7. Secret Patterns in Pipeline-Related Endpoints ───────────────────────── echo "--- Secret Pattern Scan in Accessible Endpoints ---" | tee -a "$OUT_FILE" SECRET_PATTERN='(AKIA[A-Z0-9]{16}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}|AIza[0-9A-Za-z_-]{35}|-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|password\s*[=:]\s*["\047][^"'\'']{8,}|token\s*[=:]\s*["\047][A-Za-z0-9_-]{20,})' PAGES=("$TARGET/" "$TARGET/robots.txt" "$API_TARGET/status") for PAGE in "${PAGES[@]}"; do BODY=$(curl -sk -A "$BROWSER_UA" --max-time 8 "$PAGE") if echo "$BODY" | grep -qiP "$SECRET_PATTERN"; then printf ' [CRITICAL] Secret pattern in: %s\n' "$PAGE" | tee -a "$OUT_FILE" echo "$BODY" | grep -ioP "$SECRET_PATTERN" | head -3 | sed 's/^/ /' | tee -a "$OUT_FILE" else printf ' [OK] No secret patterns in: %s\n' "$PAGE" | tee -a "$OUT_FILE" fi done echo "" | tee -a "$OUT_FILE" # ── 8. Pipeline Artifact Registry ──────────────────────────────────────────── echo "--- Internal Artifact Registries (port scan) ---" | tee -a "$OUT_FILE" HOST_IP=$(dig +short "$HOST" | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) [ -z "$HOST_IP" ] && HOST_IP="$HOST" for PORT in 8081 8082 8083 4567 4873 5000 5001 5432 3000 3001; do if timeout 2 bash -c "echo >/dev/tcp/$HOST_IP/$PORT" 2>/dev/null; then printf ' [OPEN] port %s on %s\n' "$PORT" "$HOST_IP" | tee -a "$OUT_FILE" fi done echo " (registry port scan complete)" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" echo "=== 26. CI/CD Pipeline Security COMPLETE ===" | tee -a "$OUT_FILE"