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
165 lines
5.0 KiB
Python
Executable File
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())
|