Planning implementation steps for core modules #67
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Maintenance: Workflow Guardrails [Observe]" | |
| # Rechenaufwand-Score: R=3 (K=3, L=3, N=2) | last-calibrated: 2026-08-25 | |
| # Trigger policy: repo framework score calibration for workflow cost controls. | |
| # Observe-mode governance checks for workflow safety and hygiene. | |
| # Default behavior is non-blocking: findings are reported in summary + artifact. | |
| # Optional strict mode can fail the job on high-severity findings. | |
| on: | |
| pull_request: | |
| branches: | |
| - develop | |
| - community | |
| - enterprise | |
| - hyperscaler | |
| - military | |
| - minimal | |
| paths: | |
| - '.github/workflows/**' | |
| schedule: | |
| - cron: '15 4 * * 1' | |
| workflow_dispatch: | |
| inputs: | |
| enforce_mode: | |
| description: 'Fail workflow on high-severity findings' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-maintenance-workflow-guardrails-observe-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| workflow-guardrails: | |
| name: Workflow Guardrails Observe | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Analyze workflow policies | |
| id: analyze | |
| shell: bash | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import re | |
| from pathlib import Path | |
| try: | |
| import yaml | |
| except Exception: | |
| import subprocess, sys | |
| # act / Debian 12 uses PEP 668 externally-managed Python by default. | |
| # Use the explicit break-system-packages flag so the job remains | |
| # runnable in local Docker-based emulation as well as GitHub runners. | |
| subprocess.check_call([ | |
| sys.executable, | |
| '-m', 'pip', 'install', '--quiet', '--break-system-packages', 'pyyaml' | |
| ]) | |
| import yaml | |
| root = Path('.github/workflows') | |
| files = sorted([p for p in root.glob('*.y*ml') if p.is_file()]) | |
| sha40 = re.compile(r'^[0-9a-f]{40}$') | |
| findings = [] | |
| def add(path, severity, rule, message): | |
| findings.append({ | |
| 'file': str(path).replace('\\', '/'), | |
| 'severity': severity, | |
| 'rule': rule, | |
| 'message': message, | |
| }) | |
| def has_write_perm(node): | |
| if not isinstance(node, dict): | |
| return False | |
| for _, v in node.items(): | |
| if isinstance(v, str) and v.strip().endswith('write'): | |
| return True | |
| return False | |
| for wf in files: | |
| text = wf.read_text(encoding='utf-8', errors='replace') | |
| try: | |
| doc = yaml.safe_load(text) | |
| except Exception as exc: | |
| add(wf, 'HIGH', 'yaml-parse', f'YAML parse error: {exc}') | |
| continue | |
| if not isinstance(doc, dict): | |
| add(wf, 'HIGH', 'root-mapping', 'Workflow root is not a mapping') | |
| continue | |
| top_perm = doc.get('permissions') | |
| if top_perm is None: | |
| add(wf, 'MEDIUM', 'permissions-missing', 'Missing top-level permissions block') | |
| elif isinstance(top_perm, dict) and has_write_perm(top_perm): | |
| add(wf, 'MEDIUM', 'permissions-top-write', 'Top-level permissions include write scope(s); prefer job-level write') | |
| trigger = doc.get('on') | |
| has_pr_target = False | |
| if isinstance(trigger, dict): | |
| has_pr_target = 'pull_request_target' in trigger | |
| elif isinstance(trigger, list): | |
| has_pr_target = 'pull_request_target' in trigger | |
| elif isinstance(trigger, str): | |
| has_pr_target = trigger == 'pull_request_target' | |
| if has_pr_target: | |
| # Escalate if any write permission appears in top-level or jobs. | |
| write_detected = False | |
| if isinstance(top_perm, dict) and has_write_perm(top_perm): | |
| write_detected = True | |
| jobs = doc.get('jobs', {}) | |
| if isinstance(jobs, dict): | |
| for _, job in jobs.items(): | |
| if isinstance(job, dict) and has_write_perm(job.get('permissions', {})): | |
| write_detected = True | |
| break | |
| if write_detected: | |
| add(wf, 'HIGH', 'pull-request-target-write', 'pull_request_target with write permissions requires explicit threat model review') | |
| if 'continue-on-error: true' in text: | |
| add(wf, 'LOW', 'continue-on-error', 'continue-on-error: true detected; ensure this is intentional and documented') | |
| for line in text.splitlines(): | |
| s = line.strip() | |
| if not s.startswith('uses:'): | |
| continue | |
| spec = s.split('uses:', 1)[1].strip() | |
| if '@' not in spec: | |
| add(wf, 'MEDIUM', 'uses-version-missing', f'Action reference without version: {spec}') | |
| continue | |
| action, ref = spec.split('@', 1) | |
| action = action.strip() | |
| ref = ref.strip() | |
| if action.startswith('./'): | |
| continue | |
| if action.startswith('actions/') or action.startswith('github/'): | |
| continue | |
| if not sha40.fullmatch(ref): | |
| add(wf, 'HIGH', 'third-party-unpinned', f'Third-party action not pinned to full SHA: {spec}') | |
| severity_rank = {'HIGH': 3, 'MEDIUM': 2, 'LOW': 1} | |
| findings.sort(key=lambda x: (-severity_rank[x['severity']], x['file'], x['rule'])) | |
| counts = { | |
| 'HIGH': sum(1 for f in findings if f['severity'] == 'HIGH'), | |
| 'MEDIUM': sum(1 for f in findings if f['severity'] == 'MEDIUM'), | |
| 'LOW': sum(1 for f in findings if f['severity'] == 'LOW'), | |
| } | |
| md = [] | |
| md.append('# Workflow Guardrails Observe Report') | |
| md.append('') | |
| md.append(f"Scanned files: {len(files)}") | |
| md.append(f"Findings: HIGH={counts['HIGH']}, MEDIUM={counts['MEDIUM']}, LOW={counts['LOW']}") | |
| md.append('') | |
| md.append('| Severity | File | Rule | Message |') | |
| md.append('|---|---|---|---|') | |
| if findings: | |
| for f in findings: | |
| md.append(f"| {f['severity']} | `{f['file']}` | `{f['rule']}` | {f['message']} |") | |
| else: | |
| md.append('| OK | - | - | No findings |') | |
| out_dir = Path('reports/workflow-guardrails') | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| (out_dir / 'workflow_guardrails_report.md').write_text('\n'.join(md) + '\n', encoding='utf-8') | |
| (out_dir / 'workflow_guardrails_findings.json').write_text( | |
| json.dumps({'counts': counts, 'findings': findings}, indent=2), encoding='utf-8' | |
| ) | |
| github_output = Path(__import__('os').environ['GITHUB_OUTPUT']) | |
| with github_output.open('a', encoding='utf-8') as fh: | |
| fh.write(f"high_count={counts['HIGH']}\n") | |
| fh.write(f"medium_count={counts['MEDIUM']}\n") | |
| fh.write(f"low_count={counts['LOW']}\n") | |
| summary = Path(__import__('os').environ['GITHUB_STEP_SUMMARY']) | |
| with summary.open('a', encoding='utf-8') as fh: | |
| fh.write('## Workflow Guardrails (Observe)\n\n') | |
| fh.write(f"- Files scanned: {len(files)}\n") | |
| fh.write(f"- HIGH: {counts['HIGH']}\n") | |
| fh.write(f"- MEDIUM: {counts['MEDIUM']}\n") | |
| fh.write(f"- LOW: {counts['LOW']}\n\n") | |
| fh.write('Detailed report is uploaded as artifact.\n') | |
| PY | |
| - name: Upload guardrails report artifact | |
| if: ${{ !env.ACT }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: workflow-guardrails-report-${{ github.run_id }} | |
| path: reports/workflow-guardrails/ | |
| retention-days: 30 | |
| - name: Enforce mode (optional) | |
| if: inputs.enforce_mode && steps.analyze.outputs.high_count != '0' | |
| run: | | |
| echo "::error::Workflow guardrails detected HIGH findings in enforce mode" | |
| exit 1 |