Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

165 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Validates that all GitHub Actions workflow service containers and container images
use SHA256 digests for supply chain security.
Prevents tag tampering attacks by ensuring immutable image references.
"""
import sys
from pathlib import Path
from typing import List, Tuple
try:
import yaml
except ImportError:
print("❌ Error: PyYAML is required. Install with: pip install pyyaml")
sys.exit(1)
# ANSI color codes
class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
NC = "\033[0m" # No Color
def has_sha_digest(image_ref: str) -> bool:
"""Check if image reference includes SHA256 digest."""
return "@sha256:" in image_ref
def validate_workflow(workflow_path: Path) -> List[Tuple[str, str, str]]:
"""
Validate a workflow file for unpinned images.
Returns:
List of (job_name, violation_type, image) tuples
"""
violations = []
try:
with open(workflow_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict) or "jobs" not in data:
return violations
# Check each job
for job_name, job_def in data["jobs"].items():
if not isinstance(job_def, dict):
continue
# Check container: field
if "container" in job_def:
container = job_def["container"]
# Container can be a string or dict with 'image' key
if isinstance(container, str):
if not has_sha_digest(container):
violations.append((job_name, "container", container))
elif isinstance(container, dict) and "image" in container:
image = container["image"]
if not has_sha_digest(image):
violations.append((job_name, "container", image))
# Check services: field
if "services" in job_def and isinstance(job_def["services"], dict):
for service_name, service_def in job_def["services"].items():
if isinstance(service_def, dict) and "image" in service_def:
image = service_def["image"]
if not has_sha_digest(image):
violations.append(
(job_name, f"service '{service_name}'", image)
)
except yaml.YAMLError as e:
print(f"{Colors.RED}❌ YAML parse error in {workflow_path}:{Colors.NC}")
print(f" {e}")
# Return a violation to fail the check
violations.append(("parse_error", "error", str(e)))
except Exception as e:
print(
f"{Colors.RED}❌ Error processing {workflow_path}: {e}{Colors.NC}"
)
violations.append(("error", "error", str(e)))
return violations
def main():
"""Main validation logic."""
workflows_dir = Path(".github/workflows")
if not workflows_dir.exists():
print(
f"{Colors.RED}❌ .github/workflows directory not found{Colors.NC}"
)
sys.exit(1)
print("🔍 Validating GitHub Actions workflow images...")
print()
total_violations = 0
files_checked = 0
# Process all workflow files
for workflow_file in sorted(workflows_dir.glob("*.yml")) + sorted(
workflows_dir.glob("*.yaml")
):
violations = validate_workflow(workflow_file)
if violations:
print(f"{Colors.RED}📄 {workflow_file.name}:{Colors.NC}")
for job_name, violation_type, image in violations:
print(
f"{Colors.RED} ❌ Job '{job_name}' {violation_type}: {image}{Colors.NC}"
)
print()
total_violations += len(violations)
else:
print(f"{Colors.GREEN}{workflow_file.name}{Colors.NC}")
files_checked += 1
# Summary
print("━" * 50)
print("📊 Summary:")
print(f" Files checked: {files_checked}")
print(f" Violations: {total_violations}")
print("━" * 50)
if total_violations > 0:
print()
print(
f"{Colors.RED}❌ Found {total_violations} unpinned images in workflow files{Colors.NC}"
)
print()
print("Service container images must use SHA256 digests for security.")
print()
print("To fix:")
print(" 1. Pull the image: docker pull <image:tag>")
print(
" 2. Get digest: docker inspect <image:tag> | jq -r '.[0].RepoDigests[0]'"
)
print(" 3. Update workflow:")
print()
print("Example:")
print(" services:")
print(" redis:")
print(f"{Colors.RED} image: redis:alpine # Bad{Colors.NC}")
print(
f"{Colors.GREEN} image: redis:alpine@sha256:... # Good{Colors.NC}"
)
return 1
print()
print(f"{Colors.GREEN}✅ All workflow images properly pinned{Colors.NC}")
return 0
if __name__ == "__main__":
sys.exit(main())