| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
|
| 7 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 8 |
source "$SCRIPT_DIR/../config.sh" |
| 9 |
|
| 10 |
OUT_FILE="$OUT/26_cicd_pipeline.txt" |
| 11 |
echo "=== 26. CI/CD Pipeline Security ===" | tee "$OUT_FILE" |
| 12 |
echo "Target: $TARGET" | tee -a "$OUT_FILE" |
| 13 |
echo "" | tee -a "$OUT_FILE" |
| 14 |
|
| 15 |
HOST=$(echo "$TARGET" | sed 's|https\?://||' | cut -d/ -f1) |
| 16 |
|
| 17 |
do_test() { |
| 18 |
local label="$1"; shift |
| 19 |
local code |
| 20 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$@") |
| 21 |
printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" |
| 22 |
} |
| 23 |
|
| 24 |
do_body() { |
| 25 |
local label="$1"; local pattern="$2"; shift 2 |
| 26 |
local body code |
| 27 |
body=$(curl -sk --max-time 8 "$@") |
| 28 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$@") |
| 29 |
printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" |
| 30 |
if [ -n "$pattern" ] && echo "$body" | grep -qiE "$pattern"; then |
| 31 |
echo " [WARN] Sensitive pattern found: $(echo "$body" | grep -ioE "$pattern" | head -3 | tr '\n' ' ')" | tee -a "$OUT_FILE" |
| 32 |
fi |
| 33 |
} |
| 34 |
|
| 35 |
| 36 |
echo "--- CI/CD Config File Exposure ---" | tee -a "$OUT_FILE" |
| 37 |
CI_PATHS=( |
| 38 |
"/.github/workflows/deploy.yml" |
| 39 |
"/.github/workflows/ci.yml" |
| 40 |
"/.github/workflows/release.yml" |
| 41 |
"/.github/workflows/main.yml" |
| 42 |
"/.github/workflows/build.yml" |
| 43 |
"/.gitlab-ci.yml" |
| 44 |
"/Jenkinsfile" |
| 45 |
"/.circleci/config.yml" |
| 46 |
"/.travis.yml" |
| 47 |
"/azure-pipelines.yml" |
| 48 |
"/bitbucket-pipelines.yml" |
| 49 |
"/.drone.yml" |
| 50 |
"/Dockerfile" |
| 51 |
"/docker-compose.yml" |
| 52 |
"/docker-compose.prod.yml" |
| 53 |
"/docker-compose.override.yml" |
| 54 |
"/.dockerignore" |
| 55 |
"/k8s/" |
| 56 |
"/kubernetes/" |
| 57 |
"/helm/" |
| 58 |
"/charts/" |
| 59 |
"/deploy/" |
| 60 |
"/deployment/" |
| 61 |
"/infrastructure/" |
| 62 |
"/terraform/" |
| 63 |
"/ansible/" |
| 64 |
"/.env.ci" |
| 65 |
"/.env.build" |
| 66 |
"/.env.pipeline" |
| 67 |
"/buildspec.yml" |
| 68 |
"/cloudbuild.yaml" |
| 69 |
"/.github/CODEOWNERS" |
| 70 |
"/.github/dependabot.yml" |
| 71 |
"/Makefile" |
| 72 |
"/Taskfile.yml" |
| 73 |
"/scripts/deploy.sh" |
| 74 |
"/scripts/build.sh" |
| 75 |
"/scripts/release.sh" |
| 76 |
) |
| 77 |
for CI_PATH in "${CI_PATHS[@]}"; do |
| 78 |
code=$(curl -skL -o /dev/null -w "%{http_code}" --max-time 5 -A "$BROWSER_UA" "$TARGET$CI_PATH") |
| 79 |
if [ "$code" = "200" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then |
| 80 |
printf ' [%s] %s โ EXPOSED\n' "$code" "$CI_PATH" | tee -a "$OUT_FILE" |
| 81 |
fi |
| 82 |
done |
| 83 |
echo " (CI config scan complete)" | tee -a "$OUT_FILE" |
| 84 |
echo "" | tee -a "$OUT_FILE" |
| 85 |
|
| 86 |
| 87 |
echo "--- Build Artifact Exposure ---" | tee -a "$OUT_FILE" |
| 88 |
ARTIFACT_PATHS=( |
| 89 |
"/dist/" |
| 90 |
"/build/" |
| 91 |
"/out/" |
| 92 |
"/.next/" |
| 93 |
"/.nuxt/" |
| 94 |
"/target/" |
| 95 |
"/release/" |
| 96 |
"/artifacts/" |
| 97 |
"/coverage/" |
| 98 |
"/lcov-report/" |
| 99 |
"/test-results/" |
| 100 |
"/test-output/" |
| 101 |
"/.nyc_output/" |
| 102 |
"/junit.xml" |
| 103 |
"/coverage.xml" |
| 104 |
"/test-report.xml" |
| 105 |
"/.cache/" |
| 106 |
"/node_modules/" |
| 107 |
"/vendor/" |
| 108 |
"/__pycache__/" |
| 109 |
"/.gradle/" |
| 110 |
"/.m2/" |
| 111 |
) |
| 112 |
for ART in "${ARTIFACT_PATHS[@]}"; do |
| 113 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 -A "$BROWSER_UA" "$TARGET$ART") |
| 114 |
if [ "$code" = "200" ] || [ "$code" = "403" ]; then |
| 115 |
printf ' [%s] %s\n' "$code" "$ART" | tee -a "$OUT_FILE" |
| 116 |
fi |
| 117 |
done |
| 118 |
echo " (artifact scan complete โ 403=blocked-but-exists, 200=exposed)" | tee -a "$OUT_FILE" |
| 119 |
echo "" | tee -a "$OUT_FILE" |
| 120 |
|
| 121 |
| 122 |
echo "--- Webhook / Pipeline Trigger Endpoints ---" | tee -a "$OUT_FILE" |
| 123 |
WEBHOOK_PATHS=( |
| 124 |
"/webhook" |
| 125 |
"/webhooks" |
| 126 |
"/webhook/github" |
| 127 |
"/webhook/gitlab" |
| 128 |
"/webhook/bitbucket" |
| 129 |
"/webhook/deploy" |
| 130 |
"/hooks/deploy" |
| 131 |
"/hooks/push" |
| 132 |
"/trigger" |
| 133 |
"/trigger/build" |
| 134 |
"/api/webhook" |
| 135 |
"/api/webhooks" |
| 136 |
"/ci/trigger" |
| 137 |
"/deploy/trigger" |
| 138 |
"/build/trigger" |
| 139 |
"/pipeline/trigger" |
| 140 |
) |
| 141 |
for WH in "${WEBHOOK_PATHS[@]}"; do |
| 142 |
do_test "Webhook $WH (GET)" -A "$BROWSER_UA" "$TARGET$WH" |
| 143 |
done |
| 144 |
echo "" | tee -a "$OUT_FILE" |
| 145 |
|
| 146 |
| 147 |
echo "--- Webhook Forgery (unsigned GitHub event) ---" | tee -a "$OUT_FILE" |
| 148 |
FAKE_PAYLOAD="{\"ref\":\"refs/heads/main\",\"repository\":{\"full_name\":\"${PROJECT_NAME}/test\"},\"pusher\":{\"name\":\"attacker\"}}" |
| 149 |
for WH_PATH in /webhook /webhook/github /api/webhook /hooks/push; do |
| 150 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 \ |
| 151 |
-X POST -A "$BROWSER_UA" \ |
| 152 |
-H 'Content-Type: application/json' \ |
| 153 |
-H 'X-GitHub-Event: push' \ |
| 154 |
-H 'X-GitHub-Delivery: aaaaaaaa-0000-0000-0000-000000000000' \ |
| 155 |
-d "$FAKE_PAYLOAD" "$TARGET$WH_PATH") |
| 156 |
printf '[%s] Fake GitHub push event โ %s\n' "$code" "$WH_PATH" | tee -a "$OUT_FILE" |
| 157 |
done |
| 158 |
echo "" | tee -a "$OUT_FILE" |
| 159 |
|
| 160 |
| 161 |
echo "--- GitHub API โ Repo & Branch Protection Check ---" | tee -a "$OUT_FILE" |
| 162 |
GH_REPO="${GITHUB_REPO}" |
| 163 |
GH_API="https://api.github.com" |
| 164 |
do_body "Repo visibility & settings" '"private"\|"visibility"' \ |
| 165 |
-H 'Accept: application/vnd.github+json' \ |
| 166 |
-A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO" |
| 167 |
do_body "Branch protection (main)" '"required_status_checks"\|"required_pull_request_reviews"' \ |
| 168 |
-H 'Accept: application/vnd.github+json' \ |
| 169 |
-A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO/branches/main/protection" |
| 170 |
do_body "Public repo list (unauthenticated)" '"full_name"' \ |
| 171 |
-H 'Accept: application/vnd.github+json' \ |
| 172 |
-A "pqc-pentest/1.0" "$GH_API/orgs/PQCrypta/repos?type=public&per_page=5" |
| 173 |
do_body "GitHub Actions secrets (requires auth)" '"secrets"' \ |
| 174 |
-H 'Accept: application/vnd.github+json' \ |
| 175 |
-A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO/actions/secrets" |
| 176 |
do_body "GitHub Actions workflows" '"total_count"' \ |
| 177 |
-H 'Accept: application/vnd.github+json' \ |
| 178 |
-A "pqc-pentest/1.0" "$GH_API/repos/$GH_REPO/actions/workflows" |
| 179 |
echo "" | tee -a "$OUT_FILE" |
| 180 |
|
| 181 |
| 182 |
echo "--- OIDC / Federation Token Theft Vectors ---" | tee -a "$OUT_FILE" |
| 183 |
| 184 |
do_test "GitHub OIDC token endpoint (external probe)" \ |
| 185 |
"https://pipelines.actions.githubusercontent.com/serviceHosts/token" |
| 186 |
do_test "GCP Workload Identity Federation probe" \ |
| 187 |
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@pqcrypta.iam.gserviceaccount.com:generateIdToken" |
| 188 |
| 189 |
do_test "OIDC via SSRF ?url=GitHub-Actions-OIDC" \ |
| 190 |
-A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('https://pipelines.actions.githubusercontent.com/serviceHosts/token'))")" |
| 191 |
echo "" | tee -a "$OUT_FILE" |
| 192 |
|
| 193 |
| 194 |
echo "--- Dependency Substitution / Confusion Attack Surface ---" | tee -a "$OUT_FILE" |
| 195 |
| 196 |
| 197 |
| 198 |
INTERNAL_PKG_PATTERNS=("${PROJECT_NAME}" "${PROJECT_NAME}-proxy" "${PROJECT_NAME}-api" "${PROJECT_NAME}-collector" "pqc-proxy") |
| 199 |
for PKG in "${INTERNAL_PKG_PATTERNS[@]}"; do |
| 200 |
NPM_CODE=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "https://registry.npmjs.org/$PKG") |
| 201 |
printf ' npm registry: %s โ [%s]\n' "$PKG" "$NPM_CODE" | tee -a "$OUT_FILE" |
| 202 |
done |
| 203 |
for PKG in "${INTERNAL_PKG_PATTERNS[@]}"; do |
| 204 |
CARGO_CODE=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "https://crates.io/api/v1/crates/$PKG") |
| 205 |
printf ' crates.io: %s โ [%s]\n' "$PKG" "$CARGO_CODE" | tee -a "$OUT_FILE" |
| 206 |
done |
| 207 |
echo "" | tee -a "$OUT_FILE" |
| 208 |
|
| 209 |
| 210 |
echo "--- Secret Pattern Scan in Accessible Endpoints ---" | tee -a "$OUT_FILE" |
| 211 |
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,})' |
| 212 |
PAGES=("$TARGET/" "$TARGET/robots.txt" "$API_TARGET/status") |
| 213 |
for PAGE in "${PAGES[@]}"; do |
| 214 |
BODY=$(curl -sk -A "$BROWSER_UA" --max-time 8 "$PAGE") |
| 215 |
if echo "$BODY" | grep -qiP "$SECRET_PATTERN"; then |
| 216 |
printf ' [CRITICAL] Secret pattern in: %s\n' "$PAGE" | tee -a "$OUT_FILE" |
| 217 |
echo "$BODY" | grep -ioP "$SECRET_PATTERN" | head -3 | sed 's/^/ /' | tee -a "$OUT_FILE" |
| 218 |
else |
| 219 |
printf ' [OK] No secret patterns in: %s\n' "$PAGE" | tee -a "$OUT_FILE" |
| 220 |
fi |
| 221 |
done |
| 222 |
echo "" | tee -a "$OUT_FILE" |
| 223 |
|
| 224 |
| 225 |
echo "--- Internal Artifact Registries (port scan) ---" | tee -a "$OUT_FILE" |
| 226 |
HOST_IP=$(dig +short "$HOST" | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) |
| 227 |
[ -z "$HOST_IP" ] && HOST_IP="$HOST" |
| 228 |
for PORT in 8081 8082 8083 4567 4873 5000 5001 5432 3000 3001; do |
| 229 |
if timeout 2 bash -c "echo >/dev/tcp/$HOST_IP/$PORT" 2>/dev/null; then |
| 230 |
printf ' [OPEN] port %s on %s\n' "$PORT" "$HOST_IP" | tee -a "$OUT_FILE" |
| 231 |
fi |
| 232 |
done |
| 233 |
echo " (registry port scan complete)" | tee -a "$OUT_FILE" |
| 234 |
echo "" | tee -a "$OUT_FILE" |
| 235 |
|
| 236 |
echo "=== 26. CI/CD Pipeline Security COMPLETE ===" | tee -a "$OUT_FILE" |
| 237 |
|