From ae20a8cc89aee6a20d441a02946574d3a6d37b9a Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Sat, 30 May 2026 19:51:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(rulesets):=20org=5Fpush=5Fprotection=20?= =?UTF-8?q?=E2=80=94=20native=20max=5Ffile=5Fsize=20+=20banned=20extension?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the first native org-wide push-protection ruleset. Enforced at the git layer by GitHub's own push rules; no workflow runs and no per-repo caller. Replaces the historical per-repo file-size workflow callers (those become deletable in a follow-up cleanup step). What lands ---------- - rulesets.tf: new resource `github_organization_ruleset.org_push_protection` (target = "push", ~ALL repos). Two rule blocks: `max_file_size` (hard ceiling per file) and `file_extension_restriction` (banned globs). Bypass actors are intentionally not declared so manual exemptions in the GitHub UI persist across applies. - locals.tf: new. Decodes config/rulesets-defaults.yml into a named `push_protection_defaults` local, so rulesets.tf references typed terraform values instead of raw file reads inside each resource. - variables.tf: adds `org_push_protection_enforcement` (one of disabled / evaluate / active, default "active" per the new convention). - config/rulesets-defaults.yml: `banned_file_extensions` reshaped to fnmatch glob form (`*.env`, `*.pem`, …) matching the integrations/github provider's `restricted_file_extensions` input format. `max_file_size_mb` unchanged (1 MB). - README.md: "What it manages today" table now includes the new ruleset; the Layout block adds `locals.tf`; the "next" line trims file-size limits (covered now) and adds labels and per-repo file content. Verification ------------ - tofu init -backend=false and tofu validate -> green - pre-commit run --all-files -> all hooks pass Apply ----- Requires the ORG_ADMIN token tier (gh-claude-org-admin). DRYVIST is read-only on org rulesets and will 403 on apply. Assisted-by: Claude --- README.md | 11 +++++++---- config/rulesets-defaults.yml | 21 +++++++++++---------- locals.tf | 6 ++++++ rulesets.tf | 31 +++++++++++++++++++++++++++++++ variables.tf | 19 +++++++++++++++++++ 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 locals.tf diff --git a/README.md b/README.md index 9f21d1d..45f2128 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,12 @@ defined **once** here and applied to **every** repo automatically. | Resource | Effect | | --- | --- | -| `github_organization_ruleset.markdown_lint` | Requires the markdownlint workflow in `dryvist/.github` to pass on the default branch of **every** repo in the org. Single source of truth: the workflow + `.markdownlint-cli2.yaml` both live in `dryvist/.github`. | +| `github_organization_ruleset.org_push_protection` | Native GitHub push rules enforced at the git layer (no workflow runs). Hard ceiling on individual file size and a banned-extension list, applied to every repo, every ref. Thresholds and list live in `config/rulesets-defaults.yml`. | +| `github_organization_ruleset.markdown_lint` | Requires the markdownlint workflow in the org's `.github` repo to pass on the default branch of **every** repo. Single source of truth: the workflow + `.markdownlint-cli2.yaml` both live in `.github`. | -Start small — this is the seed. Branch protection, the verified-signature -policy, file-size limits, and repo settings can move here next. +Start small — this is the seed. Branch protection, commit-message format, +the verified-signature policy, repo settings, labels, and per-repo file +content (LICENSE, CODEOWNERS) move here next. ## Layout @@ -28,7 +30,8 @@ versions.tf # terraform + provider pins, S3 backend (partial) providers.tf # github provider, GITHUB_TOKEN auth variables.tf # all input variables (no magic numbers in .tf below) data.tf # live lookups: repo IDs, org metadata — never literals -rulesets.tf # org rulesets (markdown_lint, …) +locals.tf # config/*.yml decoded into named locals for rulesets.tf +rulesets.tf # org rulesets (markdown_lint, org_push_protection, …) main.tf # multi-file entrypoint stub (resources organized by topic) outputs.tf # intentionally empty — see file header config/ # YAML thresholds + lists consumed via yamldecode(file(...)) diff --git a/config/rulesets-defaults.yml b/config/rulesets-defaults.yml index 6ecf963..008724f 100644 --- a/config/rulesets-defaults.yml +++ b/config/rulesets-defaults.yml @@ -14,14 +14,15 @@ push_protection: # rather than raising the org-wide ceiling. max_file_size_mb: 1 - # Banned file extensions. These extensions usually carry credentials or - # keys; pushes containing files with these extensions are rejected at the - # git layer. Repos that legitimately need to commit one of these (e.g. a - # test fixture) put them in a bypass-allowed path or add the actor to - # bypass. + # Banned file extensions, as fnmatch globs (the integrations/github provider + # format for restricted_file_extensions). These extensions usually carry + # credentials or keys; pushes containing files matching any of these globs + # are rejected at the git layer. Repos that legitimately need to commit one + # (e.g. a test fixture) put them in a bypass-allowed path or add the actor + # to bypass. banned_file_extensions: - - env - - pem - - key - - p12 - - pfx + - "*.env" + - "*.pem" + - "*.key" + - "*.p12" + - "*.pfx" diff --git a/locals.tf b/locals.tf new file mode 100644 index 0000000..e107f01 --- /dev/null +++ b/locals.tf @@ -0,0 +1,6 @@ +# Structured defaults consumed by org rulesets live in config/*.yml; this +# file decodes them into named locals so rulesets.tf reads them as terraform +# values, not raw file reads scattered through resource bodies. +locals { + push_protection_defaults = yamldecode(file("${path.module}/config/rulesets-defaults.yml")).push_protection +} diff --git a/rulesets.tf b/rulesets.tf index 4cb50f6..1192d8e 100644 --- a/rulesets.tf +++ b/rulesets.tf @@ -1,3 +1,34 @@ +# Org-wide native push protection — max file size + banned extensions. +# +# Enforced at the git layer by GitHub's own push rules; no workflow runs. +# Thresholds and extension list come from config/rulesets-defaults.yml via +# local.push_protection_defaults, so this resource carries no magic numbers +# or hardcoded lists. Bypass actors are managed in the GitHub UI — this +# resource does not claim ownership of them, so manual exemptions for +# specific repos or actors persist across applies. +resource "github_organization_ruleset" "org_push_protection" { + name = "org-push-protection" + target = "push" + enforcement = var.org_push_protection_enforcement + + conditions { + repository_name { + include = ["~ALL"] + exclude = [] + } + } + + rules { + max_file_size { + max_file_size = local.push_protection_defaults.max_file_size_mb + } + + file_extension_restriction { + restricted_file_extensions = local.push_protection_defaults.banned_file_extensions + } + } +} + # Org-wide markdown linting, enforced as a Required Workflow. # # Every repo's default-branch PRs must pass the markdownlint workflow that diff --git a/variables.tf b/variables.tf index 48638a6..130e0f4 100644 --- a/variables.tf +++ b/variables.tf @@ -17,3 +17,22 @@ variable "markdown_lint_enforcement" { error_message = "markdown_lint_enforcement must be one of: disabled, evaluate, active." } } + +variable "org_push_protection_enforcement" { + description = <<-EOT + Enforcement mode for the org-wide push-protection ruleset (native + max_file_size + file_extension_restriction push rules). Applies to every + repo, every ref; enforced at the git layer with no workflow runs. + + One of: disabled, evaluate, active. Defaults to "active" — new rulesets + apply enabled directly per the convention; the variable exists so a + misbehaving rule can be disabled with `-var` without a code change. + EOT + type = string + default = "active" + + validation { + condition = contains(["disabled", "evaluate", "active"], var.org_push_protection_enforcement) + error_message = "org_push_protection_enforcement must be one of: disabled, evaluate, active." + } +}