diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96351dc..e26d20a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,9 @@ on: schedule: - cron: '0 3 * * *' # nightly 03:00 UTC for adversarial-extended +permissions: + contents: read + jobs: import-smoke: diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9cafcf2..aad43ac 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -9,6 +9,9 @@ on: push: branches: [main, master] +permissions: + contents: read + jobs: pre-commit: name: Run Pre-commit Hooks diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index d82bc9c..591c02c 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -9,6 +9,9 @@ on: push: branches: [main, master] +permissions: + contents: read + jobs: secret-scan: name: Detect Secrets diff --git a/tests/security/test_github_workflow_permissions.py b/tests/security/test_github_workflow_permissions.py new file mode 100644 index 0000000..cdd4069 --- /dev/null +++ b/tests/security/test_github_workflow_permissions.py @@ -0,0 +1,44 @@ +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +WORKFLOW_ROOT = REPO_ROOT / ".github" / "workflows" +AFFECTED_WORKFLOWS = ( + "ci.yml", + "pre-commit.yml", + "secret-scan.yml", +) + + +class TestGitHubWorkflowPermissions(unittest.TestCase): + + def test_affected_workflows_declare_top_level_permissions(self): + for workflow_name in AFFECTED_WORKFLOWS: + with self.subTest(workflow=workflow_name): + workflow_text = (WORKFLOW_ROOT / workflow_name).read_text( + encoding="utf-8" + ) + jobs_index = workflow_text.find("\njobs:") + self.assertGreater( + jobs_index, + 0, + f"{workflow_name} must contain a jobs section", + ) + + permissions_block = "permissions:\n contents: read\n" + permissions_index = workflow_text.find(permissions_block) + self.assertGreaterEqual( + permissions_index, + 0, + f"{workflow_name} must declare explicit top-level permissions", + ) + self.assertLess( + permissions_index, + jobs_index, + f"{workflow_name} permissions block must stay top-level before jobs", + ) + + +if __name__ == "__main__": + unittest.main()