Skip to content

Commit 5ec343f

Browse files
authored
Add CI validation for PHP/.NET SHA hashes (#2901)
1 parent 6a716bb commit 5ec343f

3 files changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Validates SHA hashes against official upstream sources.
2+
name: Validate SHA Hashes
3+
4+
on:
5+
pull_request:
6+
branches: [main]
7+
paths:
8+
- 'platforms/**/versionsToBuild.txt'
9+
- 'images/constants.yml'
10+
- 'build/scripts/validate-*-sha.sh'
11+
12+
permissions:
13+
contents: read
14+
pull-requests: write
15+
16+
jobs:
17+
validate:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Detect changed stacks
25+
id: changes
26+
run: |
27+
BASE_REF="${{ github.base_ref }}"
28+
BASE_REF="${BASE_REF:-main}"
29+
git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
30+
CHANGED=$(git diff --name-only "origin/$BASE_REF"...HEAD || git diff --name-only HEAD~1)
31+
echo "php=$(echo "$CHANGED" | grep -qE '^(platforms/php/|images/constants.yml)' && echo true || echo false)" >> $GITHUB_OUTPUT
32+
echo "dotnet=$(echo "$CHANGED" | grep -qE '^(platforms/dotnet/|images/constants.yml)' && echo true || echo false)" >> $GITHUB_OUTPUT
33+
34+
- name: Validate PHP SHA256
35+
id: php
36+
if: steps.changes.outputs.php == 'true'
37+
run: ./build/scripts/validate-php-sha.sh images/constants.yml
38+
39+
- name: Validate .NET SHA512
40+
id: dotnet
41+
if: steps.changes.outputs.dotnet == 'true'
42+
run: ./build/scripts/validate-dotnet-sha.sh images/constants.yml
43+
44+
- name: Post summary
45+
if: always() && (steps.changes.outputs.php == 'true' || steps.changes.outputs.dotnet == 'true')
46+
uses: actions/github-script@v7
47+
with:
48+
script: |
49+
const php = '${{ steps.changes.outputs.php }}' === 'true';
50+
const dotnet = '${{ steps.changes.outputs.dotnet }}' === 'true';
51+
const phpStatus = '${{ steps.php.outcome }}';
52+
const dotnetStatus = '${{ steps.dotnet.outcome }}';
53+
const icon = s => s === 'success' ? '✅' : s === 'skipped' ? '⏭️' : '❌';
54+
55+
let body = '## SHA Validation Summary\n| Stack | Status |\n|-------|--------|\n';
56+
if (php) body += `| PHP | ${icon(phpStatus)} ${phpStatus} |\n`;
57+
if (dotnet) body += `| .NET | ${icon(dotnetStatus)} ${dotnetStatus} |\n`;
58+
59+
github.rest.issues.createComment({
60+
owner: context.repo.owner,
61+
repo: context.repo.repo,
62+
issue_number: context.issue.number,
63+
body
64+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/bin/bash
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT license.
5+
# --------------------------------------------------------------------------------------------
6+
# Validates .NET SHA512 hashes against official Microsoft releases.
7+
set -euo pipefail
8+
9+
CONSTANTS_FILE="${1:-images/constants.yml}"
10+
11+
echo "Validating .NET SHA512 hashes..."
12+
failed=0
13+
14+
# Check both runtime types
15+
for prefix in NET_CORE_APP ASPNET_CORE_APP; do
16+
while IFS=: read -r key version; do
17+
key=$(echo "$key" | tr -d ' ')
18+
version=$(echo "$version" | tr -d ' "')
19+
major_raw="${key##*_}"
20+
21+
# Convert 80 -> 8.0, 100 -> 10.0
22+
if [[ ${#major_raw} -eq 2 ]]; then
23+
major="${major_raw:0:1}.${major_raw:1}"
24+
else
25+
major="${major_raw:0:2}.${major_raw:2}"
26+
fi
27+
28+
# Get local SHA - use exact match with leading space to avoid ASPNET matching NET
29+
local_sha=$(grep -E "^[[:space:]]+${key}_SHA:" "$CONSTANTS_FILE" | awk '{print $2}' || true)
30+
[[ -n "$local_sha" ]] || { echo " $key: SKIP (no SHA defined)"; continue; }
31+
32+
# Fetch official SHA
33+
jq_filter=$([ "$prefix" = "NET_CORE_APP" ] && echo '.releases[].runtime' || echo '.releases[]."aspnetcore-runtime"')
34+
tarball=$([ "$prefix" = "NET_CORE_APP" ] && echo 'dotnet-runtime-' || echo 'aspnetcore-runtime-')
35+
36+
official=$(curl -sf --max-time 30 \
37+
"https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/${major}/releases.json" | \
38+
jq -r --arg v "$version" "${jq_filter} | select(.version==\$v) | .files[] | select(.rid==\"linux-x64\" and (.name | contains(\"${tarball}\")) and (.name | endswith(\".tar.gz\"))) | .hash // empty" 2>/dev/null | head -1 || true)
39+
40+
label=$([ "$prefix" = "NET_CORE_APP" ] && echo ".NET Runtime" || echo "ASP.NET Core")
41+
if [[ -z "$official" ]]; then
42+
echo " $label $version: WARN (could not fetch)"
43+
elif [[ "${official,,}" != "${local_sha,,}" ]]; then
44+
echo " $label $version: FAILED"
45+
echo " expected: $official"
46+
echo " got: $local_sha"
47+
failed=1
48+
else
49+
echo " $label $version: OK"
50+
fi
51+
done < <(grep -E "^[[:space:]]+${prefix}_[0-9]+:" "$CONSTANTS_FILE")
52+
done
53+
54+
[[ $failed -eq 0 ]] && echo "PASSED" || { echo "FAILED"; exit 1; }

build/scripts/validate-php-sha.sh

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/bin/bash
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT license.
5+
# --------------------------------------------------------------------------------------------
6+
#
7+
# Validates PHP SHA256 hashes in constants.yml against official php.net releases.
8+
# Usage: ./validate-php-sha.sh [path/to/constants.yml]
9+
#
10+
# Exit codes:
11+
# 0 - All SHAs validated successfully
12+
# 1 - One or more SHA mismatches found
13+
# 2 - Script error (missing dependencies, file not found, etc.)
14+
15+
set -euo pipefail
16+
17+
CONSTANTS_FILE="${1:-images/constants.yml}"
18+
19+
# Security: Restrict to expected file paths within the repository
20+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
21+
CONSTANTS_REALPATH="$(realpath -m "$CONSTANTS_FILE" 2>/dev/null || echo "")"
22+
23+
# Validate path was resolved and is within repository
24+
if [[ -z "$CONSTANTS_REALPATH" ]]; then
25+
echo "Error: Could not resolve constants file path: $CONSTANTS_FILE" >&2
26+
exit 2
27+
fi
28+
29+
case "$CONSTANTS_REALPATH" in
30+
"$REPO_ROOT"/*)
31+
# Path is safe - within repository
32+
;;
33+
*)
34+
echo "Error: Constants file must be within the repository: $REPO_ROOT" >&2
35+
exit 2
36+
;;
37+
esac
38+
39+
# Colors for output (disabled if not a terminal)
40+
if [[ -t 1 ]]; then
41+
RED='\033[0;31m'
42+
GREEN='\033[0;32m'
43+
YELLOW='\033[0;33m'
44+
NC='\033[0m' # No Color
45+
else
46+
RED=''
47+
GREEN=''
48+
YELLOW=''
49+
NC=''
50+
fi
51+
52+
# Check dependencies
53+
for cmd in curl jq grep; do
54+
if ! command -v "$cmd" &> /dev/null; then
55+
echo "Error: Required command '$cmd' not found" >&2
56+
exit 2
57+
fi
58+
done
59+
60+
# Function to fetch with retry logic
61+
fetch_with_retry() {
62+
local url="$1"
63+
local max_attempts=3
64+
local timeout=10
65+
local result=""
66+
67+
for attempt in $(seq 1 $max_attempts); do
68+
result=$(curl -sf --max-time "$timeout" --proto '=https' --tlsv1.2 "$url" 2>/dev/null) && break
69+
[[ $attempt -lt $max_attempts ]] && sleep $((attempt * 2))
70+
done
71+
72+
echo "$result"
73+
}
74+
75+
echo "Validating PHP SHA256 hashes from: $CONSTANTS_FILE"
76+
echo "=================================================="
77+
78+
failed=0
79+
validated=0
80+
81+
# Extract PHP versions and SHAs from constants.yml
82+
while IFS= read -r line; do
83+
# Match lines like: php85Version: 8.5.1 (with optional leading spaces)
84+
if [[ "$line" =~ ^[[:space:]]*php([0-9]+)Version:[[:space:]]*([0-9.]+)$ ]]; then
85+
php_key="php${BASH_REMATCH[1]}"
86+
version="${BASH_REMATCH[2]}"
87+
88+
# Security: Validate version format strictly (only digits and dots, reasonable length)
89+
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [[ ${#version} -gt 20 ]]; then
90+
echo "Warning: Skipping invalid version format: $version" >&2
91+
continue
92+
fi
93+
94+
# Get corresponding SHA from constants.yml using fixed string match
95+
sha_line=$(grep -F "${php_key}Version_SHA:" "$CONSTANTS_FILE" 2>/dev/null || true)
96+
if [[ "$sha_line" =~ _SHA:[[:space:]]*([a-f0-9]{64})$ ]]; then
97+
local_sha="${BASH_REMATCH[1]}"
98+
99+
# Security: Sanitize output to prevent log injection
100+
safe_version="${version//[^0-9.]/}"
101+
echo -n "PHP $safe_version: "
102+
103+
# Fetch official SHA from php.net with retry (URL-encode version just in case)
104+
encoded_version=$(printf '%s' "$version" | jq -sRr @uri)
105+
response=$(fetch_with_retry "https://www.php.net/releases/?json&version=${encoded_version}")
106+
107+
# Validate JSON response before parsing
108+
if [[ -n "$response" ]] && ! echo "$response" | jq empty 2>/dev/null; then
109+
echo -e "${YELLOW}WARNING - Invalid JSON response from php.net${NC}"
110+
continue
111+
fi
112+
113+
official=$(echo "$response" | jq -r '.source[] | select(.filename | endswith(".tar.xz")) | .sha256 // empty' 2>/dev/null || echo "")
114+
115+
# Security: Validate response is a valid SHA256 (64 hex chars)
116+
if [[ -n "$official" ]] && [[ ! "$official" =~ ^[a-f0-9]{64}$ ]]; then
117+
echo -e "${YELLOW}WARNING - Invalid SHA format from php.net${NC}"
118+
continue
119+
fi
120+
121+
if [[ -z "$official" ]]; then
122+
echo -e "${YELLOW}WARNING - Could not fetch from php.net${NC}"
123+
elif [[ "$official" != "$local_sha" ]]; then
124+
echo -e "${RED}FAILED${NC}"
125+
echo " Expected: $official"
126+
echo " Got: $local_sha"
127+
failed=1
128+
else
129+
echo -e "${GREEN}OK${NC}"
130+
validated=$((validated + 1))
131+
fi
132+
else
133+
# Version found but no SHA - this is likely a configuration error
134+
safe_version="${version//[^0-9.]/}"
135+
echo -e "PHP $safe_version: ${YELLOW}WARNING - Version found but ${php_key}Version_SHA is missing${NC}"
136+
fi
137+
fi
138+
done < "$CONSTANTS_FILE" || {
139+
echo "Error: Could not read constants file: $CONSTANTS_FILE" >&2
140+
exit 2
141+
}
142+
143+
echo "=================================================="
144+
echo "Validated: $validated PHP version(s)"
145+
146+
if [[ $failed -eq 1 ]]; then
147+
echo -e "${RED}VALIDATION FAILED - SHA256 mismatches detected!${NC}"
148+
exit 1
149+
else
150+
echo -e "${GREEN}VALIDATION PASSED${NC}"
151+
exit 0
152+
fi

0 commit comments

Comments
 (0)