Initial commit - 611 cybersecurity skills across all subdomains

This commit is contained in:
mukul975
2026-02-25 10:47:44 +01:00
commit 22a7ab1462
1765 changed files with 280648 additions and 0 deletions
@@ -0,0 +1,245 @@
---
name: securing-github-actions-workflows
description: >
This skill covers hardening GitHub Actions workflows against supply chain attacks,
credential theft, and privilege escalation. It addresses pinning actions to SHA digests,
minimizing GITHUB_TOKEN permissions, protecting secrets from exfiltration, preventing
script injection in workflow expressions, and implementing required reviewers for
workflow changes.
domain: cybersecurity
subdomain: devsecops
tags: [devsecops, cicd, github-actions, supply-chain, workflow-security, secure-sdlc]
version: 1.0.0
author: mahipal
license: MIT
---
# Securing GitHub Actions Workflows
## When to Use
- When GitHub Actions is the CI/CD platform and workflows need hardening against supply chain attacks
- When workflows handle secrets, deploy to production, or have elevated permissions
- When preventing script injection via untrusted PR titles, branch names, or commit messages
- When requiring audit trails and approval gates for workflow modifications
- When third-party actions pose supply chain risk through mutable version tags
**Do not use** for securing other CI/CD platforms (see platform-specific hardening guides), for application vulnerability scanning (use SAST/DAST), or for secret detection in code (use Gitleaks).
## Prerequisites
- GitHub repository with GitHub Actions enabled
- GitHub organization admin access for organization-level settings
- Understanding of GitHub Actions workflow syntax and events
## Workflow
### Step 1: Pin Actions to SHA Digests
```yaml
# INSECURE: Mutable tag can be overwritten by attacker
- uses: actions/checkout@v4
# SECURE: Pinned to immutable SHA digest
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# Use Dependabot to auto-update pinned SHAs
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
```
### Step 2: Minimize GITHUB_TOKEN Permissions
```yaml
# Set restrictive default permissions at workflow level
name: CI Pipeline
permissions: {} # Start with no permissions
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Only what's needed
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
permissions:
contents: read
deployments: write
id-token: write # For OIDC-based cloud auth
steps:
- name: Deploy
run: echo "deploying"
```
### Step 3: Prevent Script Injection
```yaml
# VULNERABLE: User-controlled input in run step
- run: echo "PR title is ${{ github.event.pull_request.title }}"
# SECURE: Use environment variable (properly escaped by shell)
- name: Process PR
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
echo "PR title is ${PR_TITLE}"
echo "PR body is ${PR_BODY}"
# SECURE: Use actions/github-script for complex operations
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
script: |
const title = context.payload.pull_request.title;
console.log(`PR title: ${title}`);
```
### Step 4: Secure Fork Pull Request Handling
```yaml
# DANGEROUS: pull_request_target runs with base repo permissions
# on: pull_request_target # AVOID unless absolutely necessary
# SAFE: pull_request runs in fork context with limited permissions
on:
pull_request:
branches: [main]
# If pull_request_target is required, never checkout PR code:
on:
pull_request_target:
types: [labeled]
jobs:
safe-job:
if: contains(github.event.pull_request.labels.*.name, 'safe-to-test')
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# NEVER do: actions/checkout with ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
# This checks out the BASE branch, not the PR
```
### Step 5: Protect Secrets and Environment Variables
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- name: Deploy with secret
env:
# Secrets are masked in logs automatically
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
# Never echo secrets
# echo "$DEPLOY_KEY" # BAD
deploy-tool --key-file <(echo "$DEPLOY_KEY")
- name: Audit secret access
run: |
# Log that secret was used without exposing it
echo "::notice::Deploy key accessed for production deployment"
```
### Step 6: Implement Workflow Change Controls
```yaml
# Require CODEOWNERS approval for workflow changes
# .github/CODEOWNERS
.github/workflows/ @security-team @platform-team
.github/actions/ @security-team @platform-team
# Organization settings:
# 1. Settings > Actions > General > Fork PR policies
# - Require approval for first-time contributors
# - Require approval for all outside collaborators
# 2. Settings > Actions > General > Workflow permissions
# - Read repository contents and packages permissions
# - Do NOT allow GitHub Actions to create and approve PRs
```
## Key Concepts
| Term | Definition |
|------|------------|
| SHA Pinning | Referencing GitHub Actions by their immutable commit SHA instead of mutable version tags |
| Script Injection | Attack where untrusted input (PR title, branch name) is interpolated into shell commands |
| GITHUB_TOKEN | Automatically generated token with configurable permissions scoped to the current repository |
| pull_request_target | Dangerous event trigger that runs in the base repo context with full permissions on fork PRs |
| Environment Protection | GitHub feature requiring manual approval before jobs accessing an environment can run |
| CODEOWNERS | File defining required reviewers for specific paths including workflow files |
| OIDC Federation | Using GitHub's OIDC token to authenticate to cloud providers without storing long-lived credentials |
## Tools & Systems
- **Dependabot**: Automated dependency updater that keeps pinned action SHAs current
- **StepSecurity Harden Runner**: GitHub Action that monitors and restricts outbound network calls from workflows
- **actionlint**: Linter for GitHub Actions workflow files that detects security issues
- **allstar**: GitHub App by OpenSSF that enforces security policies on repositories
- **scorecard**: OpenSSF tool that evaluates supply chain security practices including CI/CD
## Common Scenarios
### Scenario: Preventing Supply Chain Attack via Compromised Third-Party Action
**Context**: A widely-used GitHub Action is compromised and its v3 tag is updated to include credential-stealing code. Repositories using `@v3` automatically pull the malicious version.
**Approach**:
1. Pin all actions to SHA digests immediately across all repositories
2. Configure Dependabot for github-actions ecosystem to manage SHA updates
3. Restrict GITHUB_TOKEN permissions so even compromised actions have minimal access
4. Add StepSecurity harden-runner to detect anomalous outbound network calls
5. Review all third-party actions and replace unnecessary ones with inline scripts
6. Require CODEOWNERS approval for any changes to .github/workflows/
**Pitfalls**: SHA pinning without Dependabot means missing legitimate security updates to actions. Overly restrictive permissions can break legitimate workflows. Using `pull_request_target` for label-based gating still exposes secrets if the workflow checks out PR code.
## Output Format
```
GitHub Actions Security Audit
================================
Repository: org/web-application
Date: 2026-02-23
WORKFLOW ANALYSIS:
Total workflows: 8
Total action references: 34
SHA PINNING:
[FAIL] 12/34 actions use mutable tags instead of SHA digests
- .github/workflows/ci.yml: actions/setup-node@v4
- .github/workflows/deploy.yml: aws-actions/configure-aws-credentials@v4
PERMISSIONS:
[FAIL] 3/8 workflows have no explicit permissions (inherit default)
[WARN] 1/8 workflows request write-all permissions
SCRIPT INJECTION:
[FAIL] 2 workflow steps interpolate user input directly
- .github/workflows/pr-check.yml:23: ${{ github.event.pull_request.title }}
SECRETS:
[PASS] No secrets exposed in workflow logs
[PASS] All production deployments use environment protection
SCORE: 6/10 (Remediate 5 HIGH findings)
```
@@ -0,0 +1,52 @@
# GitHub Actions Security Templates
## Hardened Workflow Template
```yaml
name: Secure CI Pipeline
permissions: {}
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1
with:
egress-policy: audit
- name: Build
run: make build
- name: Test
run: make test
```
## Dependabot for Actions
```yaml
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
```
## CODEOWNERS for Workflow Protection
```
# .github/CODEOWNERS
.github/workflows/ @org/security-team @org/platform-team
.github/actions/ @org/security-team
.github/dependabot.yml @org/platform-team
```
@@ -0,0 +1,30 @@
# Standards Reference: Securing GitHub Actions
## NIST SSDF (SP 800-218)
### PS.1: Protect All Forms of Code
- Workflows are code and must be reviewed and protected
- Pin action dependencies to SHA digests
- Minimize GITHUB_TOKEN permissions
## CIS Software Supply Chain Security
- BD-1: Define security requirements for build processes
- BD-2: Automate security validation of build configurations
- BD-3: Pin all external dependencies to immutable references
## OWASP CI/CD Top 10 Risks
| Risk | GitHub Actions Mitigation |
|------|--------------------------|
| CICD-SEC-1: Insufficient Flow Control | Environment protection rules, CODEOWNERS |
| CICD-SEC-3: Dependency Chain Abuse | SHA pinning of actions |
| CICD-SEC-4: Poisoned Pipeline Execution | Restrict pull_request_target, input sanitization |
| CICD-SEC-6: Credential Hygiene | OIDC federation, minimal GITHUB_TOKEN scope |
| CICD-SEC-9: Artifact Integrity | Sign artifacts in workflows |
## SLSA Framework
- Level 2: Hosted build service (GitHub Actions qualifies)
- Level 3: Hardened build platform with isolation guarantees
- Workflow hardening prevents provenance falsification
@@ -0,0 +1,40 @@
# Workflow Reference: Securing GitHub Actions
## Hardening Checklist
1. Pin all actions to SHA digests
2. Set restrictive default permissions
3. Sanitize all user-controlled inputs
4. Never use pull_request_target with PR checkout
5. Enable environment protection for production
6. Configure CODEOWNERS for workflow files
7. Enable Dependabot for github-actions
8. Audit third-party actions quarterly
9. Use OIDC instead of long-lived cloud credentials
10. Add harden-runner for network monitoring
## Permission Scoping Reference
| Permission | Use Case |
|-----------|----------|
| contents: read | Checkout code |
| contents: write | Create releases, push tags |
| security-events: write | Upload SARIF results |
| packages: write | Push container images |
| deployments: write | Create deployment status |
| id-token: write | OIDC cloud authentication |
| pull-requests: write | Comment on PRs |
## Script Injection Prevention
```yaml
# DANGEROUS patterns to avoid:
run: echo "${{ github.event.issue.title }}"
run: echo "${{ github.event.comment.body }}"
run: echo "${{ github.head_ref }}"
# SAFE alternatives:
env:
TITLE: ${{ github.event.issue.title }}
run: echo "${TITLE}"
```
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
GitHub Actions Workflow Security Audit Script
Analyzes workflow files for security issues including unpinned actions,
excessive permissions, script injection risks, and insecure patterns.
Usage:
python process.py --workflows-dir .github/workflows/ --output audit-report.json
"""
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
import yaml
@dataclass
class SecurityFinding:
file: str
line: int
check: str
severity: str
message: str
remediation: str
SHA_PATTERN = re.compile(r"@[0-9a-f]{40}")
TAG_PATTERN = re.compile(r"@v?\d+(\.\d+)*$")
INJECTION_PATTERN = re.compile(r"\$\{\{\s*github\.event\.(issue|pull_request|comment|review)\.\w+")
DANGEROUS_CONTEXTS = [
"github.event.issue.title",
"github.event.issue.body",
"github.event.pull_request.title",
"github.event.pull_request.body",
"github.event.comment.body",
"github.event.review.body",
"github.head_ref",
]
def load_workflow(filepath: str) -> dict:
"""Load a GitHub Actions workflow YAML file."""
try:
with open(filepath, "r") as f:
return yaml.safe_load(f) or {}
except (yaml.YAMLError, FileNotFoundError):
return {}
def check_action_pinning(workflow: dict, filepath: str) -> list:
"""Check if actions are pinned to SHA digests."""
findings = []
filename = os.path.basename(filepath)
for job_name, job in workflow.get("jobs", {}).items():
for i, step in enumerate(job.get("steps", [])):
uses = step.get("uses", "")
if not uses or uses.startswith("./"):
continue
if not SHA_PATTERN.search(uses):
findings.append(SecurityFinding(
file=filename, line=0,
check="ACTION_PINNING",
severity="HIGH",
message=f"Job '{job_name}' step {i}: '{uses}' not pinned to SHA digest",
remediation=f"Pin to SHA: {uses.split('@')[0]}@<commit-sha>"
))
return findings
def check_permissions(workflow: dict, filepath: str) -> list:
"""Check for overly permissive GITHUB_TOKEN permissions."""
findings = []
filename = os.path.basename(filepath)
top_perms = workflow.get("permissions")
if top_perms is None:
findings.append(SecurityFinding(
file=filename, line=0,
check="PERMISSIONS",
severity="MEDIUM",
message="No top-level permissions defined. Inherits default (may be write-all).",
remediation="Add 'permissions: {}' at workflow level and grant per-job."
))
elif top_perms == "write-all" or (isinstance(top_perms, dict) and
all(v == "write" for v in top_perms.values())):
findings.append(SecurityFinding(
file=filename, line=0,
check="PERMISSIONS",
severity="HIGH",
message="Workflow has write-all permissions.",
remediation="Restrict to minimum required permissions per job."
))
return findings
def check_script_injection(workflow: dict, filepath: str) -> list:
"""Check for script injection via user-controlled inputs."""
findings = []
filename = os.path.basename(filepath)
for job_name, job in workflow.get("jobs", {}).items():
for i, step in enumerate(job.get("steps", [])):
run_cmd = step.get("run", "")
if not run_cmd:
continue
for ctx in DANGEROUS_CONTEXTS:
if f"${{{{ {ctx}" in run_cmd or f"${{{{{ctx}" in run_cmd:
findings.append(SecurityFinding(
file=filename, line=0,
check="SCRIPT_INJECTION",
severity="CRITICAL",
message=f"Job '{job_name}' step {i}: '{ctx}' interpolated in run step",
remediation="Use env variable: env: VAR: ${{ " + ctx + " }} then ${VAR}"
))
return findings
def check_pr_target(workflow: dict, filepath: str) -> list:
"""Check for dangerous pull_request_target usage."""
findings = []
filename = os.path.basename(filepath)
triggers = workflow.get("on", {})
if isinstance(triggers, dict) and "pull_request_target" in triggers:
for job_name, job in workflow.get("jobs", {}).items():
for step in job.get("steps", []):
uses = step.get("uses", "")
if "checkout" in uses:
with_ref = step.get("with", {}).get("ref", "")
if "pull_request" in with_ref or "head" in with_ref:
findings.append(SecurityFinding(
file=filename, line=0,
check="PR_TARGET_CHECKOUT",
severity="CRITICAL",
message=f"Job '{job_name}': pull_request_target with PR code checkout",
remediation="Never checkout PR code in pull_request_target workflows."
))
return findings
def main():
parser = argparse.ArgumentParser(description="GitHub Actions Security Audit")
parser.add_argument("--workflows-dir", required=True)
parser.add_argument("--output", default="actions-security-report.json")
parser.add_argument("--fail-on-findings", action="store_true")
args = parser.parse_args()
workflows_dir = os.path.abspath(args.workflows_dir)
all_findings = []
workflow_files = list(Path(workflows_dir).glob("*.yml")) + list(Path(workflows_dir).glob("*.yaml"))
print(f"[*] Auditing {len(workflow_files)} workflow files in {workflows_dir}")
for wf_path in workflow_files:
workflow = load_workflow(str(wf_path))
if not workflow:
continue
all_findings.extend(check_action_pinning(workflow, str(wf_path)))
all_findings.extend(check_permissions(workflow, str(wf_path)))
all_findings.extend(check_script_injection(workflow, str(wf_path)))
all_findings.extend(check_pr_target(workflow, str(wf_path)))
severity_counts = {}
for f in all_findings:
severity_counts[f.severity] = severity_counts.get(f.severity, 0) + 1
report = {
"metadata": {
"directory": workflows_dir,
"date": datetime.now(timezone.utc).isoformat(),
"workflows_scanned": len(workflow_files)
},
"summary": {
"total_findings": len(all_findings),
"severity_counts": severity_counts
},
"findings": [
{"file": f.file, "check": f.check, "severity": f.severity,
"message": f.message, "remediation": f.remediation}
for f in sorted(all_findings,
key=lambda x: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}.get(x.severity, 4))
]
}
output_path = os.path.abspath(args.output)
with open(output_path, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Report: {output_path}")
for f in all_findings:
print(f" [{f.severity}] {f.file}: {f.message}")
passed = len(all_findings) == 0
print(f"\n[{'PASS' if passed else 'FAIL'}] {len(all_findings)} security findings")
if args.fail_on_findings and not passed:
sys.exit(1)
if __name__ == "__main__":
main()