mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
Complete folder anatomy for all 649 cybersecurity skills + update LICENSE to Mahipal
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for securing GitHub Actions workflows.
|
||||
|
||||
Audits GitHub Actions workflow files for security issues including
|
||||
unpinned actions, excessive permissions, script injection risks,
|
||||
dangerous triggers, and missing secret protections.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
class GitHubActionsSecurityAgent:
|
||||
"""Audits GitHub Actions workflows for security vulnerabilities."""
|
||||
|
||||
def __init__(self, repo_path=".", output_dir="./gha_audit"):
|
||||
self.repo_path = Path(repo_path)
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.findings = []
|
||||
|
||||
def _load_workflow(self, path):
|
||||
if yaml:
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
with open(path) as f:
|
||||
return {"raw": f.read()}
|
||||
|
||||
def find_workflows(self):
|
||||
"""Discover all workflow files in the repository."""
|
||||
wf_dir = self.repo_path / ".github" / "workflows"
|
||||
if not wf_dir.exists():
|
||||
return []
|
||||
return sorted(wf_dir.glob("*.yml")) + sorted(wf_dir.glob("*.yaml"))
|
||||
|
||||
def check_sha_pinning(self, workflow_path, content):
|
||||
"""Check if actions are pinned to SHA digests."""
|
||||
unpinned = []
|
||||
raw = content.get("raw", "") if "raw" in content else ""
|
||||
if not raw:
|
||||
try:
|
||||
raw = Path(workflow_path).read_text()
|
||||
except Exception:
|
||||
return unpinned
|
||||
|
||||
for line_num, line in enumerate(raw.splitlines(), 1):
|
||||
m = re.search(r'uses:\s+([^@\s]+)@([^\s#]+)', line)
|
||||
if m:
|
||||
action, ref = m.group(1), m.group(2)
|
||||
if not re.match(r'^[a-f0-9]{40}$', ref):
|
||||
unpinned.append({
|
||||
"action": action,
|
||||
"ref": ref,
|
||||
"line": line_num,
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
self.findings.append({
|
||||
"severity": "medium",
|
||||
"type": "Unpinned Action",
|
||||
"detail": f"{action}@{ref} at line {line_num}",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
return unpinned
|
||||
|
||||
def check_permissions(self, workflow_path, content):
|
||||
"""Check for overly permissive GITHUB_TOKEN permissions."""
|
||||
issues = []
|
||||
if not isinstance(content, dict) or "raw" in content:
|
||||
return issues
|
||||
|
||||
top_perms = content.get("permissions")
|
||||
if top_perms is None:
|
||||
issues.append({
|
||||
"issue": "No top-level permissions defined (inherits defaults)",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
self.findings.append({
|
||||
"severity": "medium",
|
||||
"type": "Missing Permissions",
|
||||
"detail": "Workflow has no permissions block",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
|
||||
if top_perms == "write-all" or (isinstance(top_perms, dict) and
|
||||
top_perms.get("contents") == "write" and
|
||||
top_perms.get("actions") == "write"):
|
||||
issues.append({"issue": "Overly permissive write-all", "file": str(workflow_path)})
|
||||
self.findings.append({
|
||||
"severity": "high",
|
||||
"type": "Excessive Permissions",
|
||||
"detail": "write-all permissions granted",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def check_script_injection(self, workflow_path, content):
|
||||
"""Check for user-controlled input in run steps (script injection)."""
|
||||
injections = []
|
||||
raw = content.get("raw", "") if "raw" in content else ""
|
||||
if not raw:
|
||||
try:
|
||||
raw = Path(workflow_path).read_text()
|
||||
except Exception:
|
||||
return injections
|
||||
|
||||
dangerous_contexts = [
|
||||
"github.event.pull_request.title",
|
||||
"github.event.pull_request.body",
|
||||
"github.event.issue.title",
|
||||
"github.event.issue.body",
|
||||
"github.event.comment.body",
|
||||
"github.event.review.body",
|
||||
"github.head_ref",
|
||||
]
|
||||
|
||||
in_run = False
|
||||
for line_num, line in enumerate(raw.splitlines(), 1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("run:") or stripped.startswith("run: |"):
|
||||
in_run = True
|
||||
elif in_run and not stripped.startswith("-") and not stripped.startswith("#"):
|
||||
for ctx in dangerous_contexts:
|
||||
if f"${{{{ {ctx}" in line or f"${{{{{ctx}" in line:
|
||||
injections.append({
|
||||
"context": ctx,
|
||||
"line": line_num,
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
self.findings.append({
|
||||
"severity": "high",
|
||||
"type": "Script Injection",
|
||||
"detail": f"{ctx} in run step at line {line_num}",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
if stripped and not stripped.startswith("-") and not stripped.startswith("#") and ":" in stripped and not stripped.startswith("run"):
|
||||
in_run = False
|
||||
|
||||
return injections
|
||||
|
||||
def check_dangerous_triggers(self, workflow_path, content):
|
||||
"""Check for dangerous event triggers."""
|
||||
issues = []
|
||||
if not isinstance(content, dict) or "raw" in content:
|
||||
raw = content.get("raw", "")
|
||||
if "pull_request_target" in raw:
|
||||
issues.append({"trigger": "pull_request_target", "file": str(workflow_path)})
|
||||
self.findings.append({
|
||||
"severity": "high",
|
||||
"type": "Dangerous Trigger",
|
||||
"detail": "pull_request_target allows fork code to run with base permissions",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
return issues
|
||||
|
||||
on_block = content.get("on", content.get(True, {}))
|
||||
if isinstance(on_block, dict) and "pull_request_target" in on_block:
|
||||
issues.append({"trigger": "pull_request_target", "file": str(workflow_path)})
|
||||
self.findings.append({
|
||||
"severity": "high",
|
||||
"type": "Dangerous Trigger",
|
||||
"detail": "pull_request_target trigger used",
|
||||
"file": str(workflow_path),
|
||||
})
|
||||
return issues
|
||||
|
||||
def audit_all(self):
|
||||
"""Run all security checks on all workflow files."""
|
||||
workflows = self.find_workflows()
|
||||
results = []
|
||||
for wf in workflows:
|
||||
content = self._load_workflow(wf)
|
||||
unpinned = self.check_sha_pinning(wf, content)
|
||||
perms = self.check_permissions(wf, content)
|
||||
injections = self.check_script_injection(wf, content)
|
||||
triggers = self.check_dangerous_triggers(wf, content)
|
||||
results.append({
|
||||
"workflow": str(wf),
|
||||
"unpinned_actions": len(unpinned),
|
||||
"permission_issues": len(perms),
|
||||
"script_injections": len(injections),
|
||||
"dangerous_triggers": len(triggers),
|
||||
})
|
||||
return results
|
||||
|
||||
def generate_report(self):
|
||||
audit = self.audit_all()
|
||||
report = {
|
||||
"report_date": datetime.utcnow().isoformat(),
|
||||
"repository": str(self.repo_path),
|
||||
"workflows_scanned": len(audit),
|
||||
"audit_summary": audit,
|
||||
"findings": self.findings,
|
||||
"total_findings": len(self.findings),
|
||||
}
|
||||
out = self.output_dir / "gha_security_report.json"
|
||||
with open(out, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(json.dumps(report, indent=2))
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
repo = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
agent = GitHubActionsSecurityAgent(repo)
|
||||
agent.generate_report()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user