Skip to content

Commit 37a4f20

Browse files
williabyclaude
andcommitted
feat(template): add OpenSSF security workflows and project setup guide
Add four new security/compliance workflows: - OpenSSF Scorecard for supply chain security assessment - REUSE Compliance for license management (conditional on use_reuse_licensing) - SBOM & Security Scan for software bill of materials and Trivy scanning Add corresponding badges to README: - OpenSSF Scorecard badge (Quality & Security section) - REUSE Compliance badge (conditional) - SBOM & Security Scan badge (CI/CD section) Add comprehensive PROJECT_SETUP.md guide covering: - Initial setup steps after template generation - Manual registrations (OpenSSF Best Practices, Codecov, SonarCloud) - Keeping project updated with cruft - CI/CD workflow configuration - Badge configuration and setup - Security configuration and branch protection - Repository management best practices Update post-generation hook to clean up reuse.yml when REUSE licensing disabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0b9938a commit 37a4f20

6 files changed

Lines changed: 684 additions & 0 deletions

File tree

hooks/post_gen_project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def cleanup_conditional_files() -> None:
113113
if "{{ cookiecutter.use_reuse_licensing }}" == "no":
114114
remove_file(Path("REUSE.toml"))
115115
remove_dir(Path("LICENSES"))
116+
remove_file(Path(".github/workflows/reuse.yml"))
116117

117118
# Remove Docker files if not needed
118119
if "{{ cookiecutter.include_docker }}" == "no":
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# REUSE Compliance Check for {{ cookiecutter.project_name }}
2+
# Validates REUSE 3.0 specification compliance for license management
3+
#
4+
# REUSE helps with clear licensing by using SPDX headers
5+
# Documentation: https://reuse.software/
6+
7+
{%- if cookiecutter.use_reuse_licensing == "yes" and cookiecutter.include_github_actions == "yes" %}
8+
name: REUSE Compliance
9+
10+
on:
11+
pull_request:
12+
paths:
13+
- "**/*"
14+
- "REUSE.toml"
15+
- "LICENSES/**"
16+
- ".github/workflows/reuse.yml"
17+
push:
18+
branches:
19+
- main
20+
- master
21+
- develop
22+
23+
permissions: read-all
24+
25+
jobs:
26+
reuse:
27+
name: Check REUSE Compliance
28+
runs-on: ubuntu-latest
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
32+
33+
- name: REUSE Compliance Check
34+
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
35+
36+
- name: Generate REUSE SPDX
37+
if: success()
38+
run: |
39+
docker run --rm --volume $(pwd):/data fsfe/reuse:latest spdx --output /data/reuse-spdx.json
40+
41+
- name: Upload REUSE SPDX
42+
if: success()
43+
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
44+
with:
45+
name: reuse-spdx
46+
path: reuse-spdx.json
47+
retention-days: 90
48+
49+
validate-licenses:
50+
name: Validate License Files
51+
runs-on: ubuntu-latest
52+
steps:
53+
- name: Checkout repository
54+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
55+
56+
- name: Check primary license exists
57+
run: |
58+
if [ ! -f "LICENSES/{{ cookiecutter.license }}.txt" ]; then
59+
echo "Warning: {{ cookiecutter.license }}.txt not found in LICENSES/"
60+
echo "You may need to download it from https://spdx.org/licenses/"
61+
fi
62+
63+
- name: Verify REUSE.toml exists
64+
run: |
65+
if [ ! -f "REUSE.toml" ]; then
66+
echo "Error: REUSE.toml missing from repository root"
67+
exit 1
68+
fi
69+
{%- else %}
70+
# REUSE licensing disabled for this project
71+
# Enable use_reuse_licensing and include_github_actions to use REUSE compliance checks
72+
{%- endif %}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# SBOM & Security Scan for {{ cookiecutter.project_name }}
2+
# Software Bill of Materials generation and vulnerability scanning
3+
#
4+
# Generates CycloneDX SBOMs and scans with Trivy for vulnerabilities
5+
# Documentation: https://cyclonedx.org/
6+
7+
{%- if cookiecutter.include_github_actions == "yes" %}
8+
name: SBOM & Security Scan
9+
10+
on:
11+
pull_request:
12+
paths:
13+
- "pyproject.toml"
14+
- "uv.lock"
15+
- ".github/workflows/sbom.yml"
16+
push:
17+
branches:
18+
- main
19+
- master
20+
- develop
21+
paths:
22+
- "pyproject.toml"
23+
- "uv.lock"
24+
schedule:
25+
# Weekly scan every Monday at 8:00 AM UTC
26+
- cron: "0 8 * * 1"
27+
workflow_dispatch:
28+
29+
permissions: read-all
30+
31+
jobs:
32+
generate-sbom:
33+
name: Generate SBOMs
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
41+
with:
42+
python-version: "{{ cookiecutter.python_version }}"
43+
44+
- name: Install UV
45+
uses: astral-sh/setup-uv@v4
46+
with:
47+
enable-cache: true
48+
cache-dependency-glob: "uv.lock"
49+
50+
- name: Upgrade pip
51+
run: python -m pip install --upgrade pip
52+
53+
- name: Install cyclonedx-bom
54+
run: pip install cyclonedx-bom==4.6.1
55+
56+
- name: Install dependencies
57+
run: uv sync --frozen
58+
59+
- name: Generate runtime SBOM (production dependencies)
60+
run: |
61+
cyclonedx-py environment \
62+
--of json \
63+
-o sbom-runtime.json \
64+
.venv
65+
66+
- name: Upload SBOM artifacts
67+
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
68+
with:
69+
name: sboms
70+
path: sbom-*.json
71+
retention-days: 90
72+
73+
scan-runtime:
74+
name: Scan Runtime Dependencies
75+
needs: generate-sbom
76+
runs-on: ubuntu-latest
77+
permissions:
78+
contents: read
79+
security-events: write # Required for SARIF upload
80+
steps:
81+
- name: Download SBOM artifacts
82+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
83+
with:
84+
name: sboms
85+
86+
- name: Trivy SBOM scan (runtime)
87+
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
88+
with:
89+
scan-type: sbom
90+
scan-ref: sbom-runtime.json
91+
format: table
92+
severity: CRITICAL,HIGH
93+
exit-code: 1 # Fail on CRITICAL/HIGH vulnerabilities
94+
95+
- name: Trivy SBOM scan (runtime - detailed report)
96+
if: always()
97+
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
98+
with:
99+
scan-type: sbom
100+
scan-ref: sbom-runtime.json
101+
format: sarif
102+
output: trivy-runtime-results.sarif
103+
104+
- name: Upload Trivy results to GitHub Security tab
105+
if: always()
106+
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.27.5
107+
with:
108+
sarif_file: trivy-runtime-results.sarif
109+
category: trivy-runtime-deps
110+
111+
license-compliance:
112+
name: License Compliance Check
113+
needs: generate-sbom
114+
runs-on: ubuntu-latest
115+
steps:
116+
- name: Download SBOM artifacts
117+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
118+
with:
119+
name: sboms
120+
121+
- name: Set up Python
122+
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
123+
with:
124+
python-version: "{{ cookiecutter.python_version }}"
125+
126+
- name: Check licenses in runtime SBOM
127+
run: |
128+
echo "Checking runtime dependency licenses..."
129+
python -c "
130+
import json
131+
with open('sbom-runtime.json') as f:
132+
sbom = json.load(f)
133+
134+
# Use precise SPDX license identifiers (exact match)
135+
forbidden_licenses = [
136+
'GPL-2.0-only', 'GPL-2.0-or-later',
137+
'GPL-3.0-only', 'GPL-3.0-or-later',
138+
'AGPL-3.0-only', 'AGPL-3.0-or-later'
139+
]
140+
issues = []
141+
142+
for component in sbom.get('components', []):
143+
licenses = component.get('licenses', [])
144+
for lic in licenses:
145+
lic_id = lic.get('license', {}).get('id', '')
146+
if lic_id in forbidden_licenses:
147+
issues.append(f\"{component['name']}: {lic_id}\")
148+
149+
if issues:
150+
print('WARNING: Found potentially incompatible licenses:')
151+
for issue in issues:
152+
print(f' - {issue}')
153+
# Don't fail, just warn
154+
else:
155+
print('All licenses compatible')
156+
"
157+
{%- else %}
158+
# GitHub Actions disabled for this project
159+
# Enable include_github_actions to use SBOM generation and security scanning
160+
{%- endif %}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# OpenSSF Scorecard for {{ cookiecutter.project_name }}
2+
# Security posture assessment and supply chain security validation
3+
#
4+
# Dashboard: https://securityscorecards.dev/viewer/?uri=github.com/{{ cookiecutter.github_org_or_user }}/{{ cookiecutter.project_slug }}
5+
# Documentation: https://github.com/ossf/scorecard
6+
7+
{%- if cookiecutter.include_github_actions == "yes" %}
8+
name: OpenSSF Scorecard
9+
10+
on:
11+
branch_protection_rule:
12+
schedule:
13+
# Weekly on Tuesdays at 21:21 UTC
14+
- cron: '21 21 * * 2'
15+
push:
16+
branches:
17+
- main
18+
- master
19+
workflow_dispatch:
20+
21+
# Minimal permissions for scorecard execution
22+
permissions: read-all
23+
24+
jobs:
25+
analysis:
26+
name: Scorecard Analysis
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 15
29+
30+
permissions:
31+
# Required for uploading results to GitHub Code Scanning
32+
security-events: write
33+
# Required for OIDC token generation (scorecard uses this for trusted publishing)
34+
id-token: write
35+
# Required for reading repository contents
36+
contents: read
37+
# Required for reading branch protection rules
38+
actions: read
39+
40+
steps:
41+
- name: Harden the runner (Audit outbound calls)
42+
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
43+
with:
44+
egress-policy: audit
45+
# Allow scorecard to make necessary API calls
46+
allowed-endpoints: >
47+
api.github.com:443
48+
github.com:443
49+
api.scorecard.dev:443
50+
*.githubusercontent.com:443
51+
*.actions.githubusercontent.com:443
52+
index.docker.io:443
53+
registry-1.docker.io:443
54+
auth.docker.io:443
55+
56+
- name: Checkout repository
57+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
58+
with:
59+
persist-credentials: false
60+
fetch-depth: 0 # Full history for accurate analysis
61+
62+
- name: Run OpenSSF Scorecard
63+
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
64+
with:
65+
results_file: scorecard-results.sarif
66+
results_format: sarif
67+
# Publish results for public repositories
68+
publish_results: true
69+
# Optional: Use GitHub PAT for more accurate branch protection checks
70+
# repo_token: {{ "${{ secrets.SCORECARD_TOKEN }}" }}
71+
72+
- name: Upload Scorecard results to GitHub Code Scanning
73+
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.27.5
74+
with:
75+
sarif_file: scorecard-results.sarif
76+
77+
- name: Upload Scorecard results as artifact
78+
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
79+
with:
80+
name: scorecard-results
81+
path: scorecard-results.sarif
82+
retention-days: 5
83+
if-no-files-found: error
84+
{%- else %}
85+
# GitHub Actions disabled for this project
86+
# Enable include_github_actions to use OpenSSF Scorecard
87+
{%- endif %}

{{cookiecutter.project_slug}}/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Quality & Security
44

5+
{%- if cookiecutter.include_github_actions == "yes" %}
6+
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/badge)](https://securityscorecards.dev/viewer/?uri=github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}})
7+
{%- endif %}
58
{%- if cookiecutter.include_codecov == "yes" %}
69
[![codecov](https://codecov.io/gh/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/graph/badge.svg)](https://codecov.io/gh/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}})
710
{%- endif %}
@@ -10,6 +13,9 @@
1013
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project={{cookiecutter.github_org_or_user}}_{{cookiecutter.project_slug}}&metric=security_rating)](https://sonarcloud.io/summary/new_code?id={{cookiecutter.github_org_or_user}}_{{cookiecutter.project_slug}})
1114
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project={{cookiecutter.github_org_or_user}}_{{cookiecutter.project_slug}}&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id={{cookiecutter.github_org_or_user}}_{{cookiecutter.project_slug}})
1215
{%- endif %}
16+
{%- if cookiecutter.use_reuse_licensing == "yes" and cookiecutter.include_github_actions == "yes" %}
17+
[![REUSE Compliance](https://github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/actions/workflows/reuse.yml/badge.svg)](https://github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/actions/workflows/reuse.yml)
18+
{%- endif %}
1319

1420
## CI/CD Status
1521

@@ -23,6 +29,9 @@
2329
{%- if cookiecutter.include_fuzzing == "yes" %}
2430
[![ClusterFuzzLite](https://github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/actions/workflows/cifuzzy.yml/badge.svg?branch=master)](https://github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/actions/workflows/cifuzzy.yml?query=branch%3Amaster)
2531
{%- endif %}
32+
{%- if cookiecutter.include_github_actions == "yes" %}
33+
[![SBOM & Security Scan](https://github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/actions/workflows/sbom.yml/badge.svg?branch=master)](https://github.com/{{cookiecutter.github_org_or_user}}/{{cookiecutter.project_slug}}/actions/workflows/sbom.yml?query=branch%3Amaster)
34+
{%- endif %}
2635

2736
## Project Info
2837

0 commit comments

Comments
 (0)