diff --git a/README.md b/README.md index 0f2af72..58fe43b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A [Terraform] module for creating a public or private repository on [Github]. - [Resources](#resources) - [Inputs](#inputs) - [Outputs](#outputs) + - [Security And Analysis Configuration](#security-and-analysis-configuration) - [External Documentation](#external-documentation) - [Terraform Github Provider Documentation](#terraform-github-provider-documentation) - [Module Versioning](#module-versioning) @@ -87,13 +88,13 @@ See [variables.tf] and [examples/] for details and use-cases. | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3 | -| [github](#requirement\_github) | >= 6.2, < 7.0 | +| [github](#requirement\_github) | >= 6.2, < 6.9 | ### Providers | Name | Version | |------|---------| -| [github](#provider\_github) | >= 6.2, < 7.0 | +| [github](#provider\_github) | >= 6.2, < 6.9 | ### Resources @@ -178,6 +179,7 @@ See [variables.tf] and [examples/] for details and use-cases. | [push\_team\_ids](#input\_push\_team\_ids) | (Optional) A list of teams (by id) to grant push (read-write) permission to. | `list(string)` | `[]` | no | | [push\_teams](#input\_push\_teams) | (Optional) A list of teams (by name/slug) to grant push (read-write) permission to. | `list(string)` | `[]` | no | | [rulesets](#input\_rulesets) | (Optional) A list of branch rulesets to apply to the repository. Default is [].

It is very likely removal of any section will require setting it to an empty list/map.
This is due to limitations in the API whereby components are not destroyed upon removal. |
list(
object({
enforcement = string
name = string
target = string

rules = list(
object({
creation = optional(bool)
deletion = optional(bool)
non_fast_forward = optional(bool)
required_signatures = optional(bool)
required_linear_history = optional(bool)
update = optional(bool)
update_allows_fetch_and_merge = optional(bool)

branch_name_pattern = optional(
object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
})
)

commit_author_email_pattern = optional(
object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
})
)

commit_message_pattern = optional(
object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
})
)

committer_email_pattern = optional(
object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
})
)

tag_name_pattern = optional(
object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
})
)

required_status_checks = optional(
object({
strict_required_status_checks_policy = optional(bool)
do_not_enforce_on_create = optional(bool)
required_check = list(
object({
context = string
integration_id = optional(number)
})
)
})
)

pull_request = optional(
object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
})
)

required_workflows = optional(
object({
required_workflow = list(
object({
repository_id = number
ref = string
path = string
})
)
})
)

required_deployments = optional(
object({
required_deployment_environments = list(string)
})
)

required_code_scanning = optional(
object({
required_code_scanning_tool = list(
object({
tool = string
alerts_threshold = string
security_alerts_threshold = string
})
)
})
)

merge_queue = optional(
object({
check_response_timeout_minutes = optional(number)
grouping_strategy = optional(string)
max_entries_to_build = optional(number)
max_entries_to_merge = optional(number)
merge_method = optional(string)
min_entries_to_merge = optional(number)
min_entries_to_merge_wait_minutes = optional(number)
})
)
})
)

bypass_actors = optional(
list(
object({
actor_id = optional(number)
actor_type = string
bypass_mode = optional(string)
})
)
)

conditions = optional(
object({
ref_name = object({
include = list(string)
exclude = list(string)
})
})
)
})
)
| `[]` | no | +| [security\_and\_analysis](#input\_security\_and\_analysis) | (Optional) Security and analysis configuration for the repository.

- All fields except org\_advanced\_security are strings: "enabled" or "disabled".
- org\_advanced\_security is a bool indicating whether the org has split licensing for Advanced Security. |
object({
org_advanced_security = optional(bool, false)
advanced_security = optional(string, "disabled")
code_security = optional(string, "disabled")
secret_scanning = optional(string, "disabled")
secret_scanning_push_protection = optional(string, "disabled")
secret_scanning_ai_detection = optional(string, "disabled")
secret_scanning_non_provider_patterns = optional(string, "disabled")
})
| `null` | no | | [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. | `string` | `"COMMIT_MESSAGES"` | no | | [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. | `string` | `"COMMIT_OR_PR_TITLE"` | no | | [template](#input\_template) | (Optional) Template repository to use. (Default: {}) |
object({
owner = string
repository = string
})
| `null` | no | @@ -210,6 +212,41 @@ See [variables.tf] and [examples/] for details and use-cases. | [webhooks](#output\_webhooks) | All attributes and arguments as returned by the github\_repository\_webhook resource. | +### Security And Analysis Configuration + +- [**`security_and_analysis`**](#var-security_and_analysis): *(Optional `object(security_and_analysis)`)* + + (Optional) The repository's [security and analysis](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-security-and-analysis-settings-for-your-repository) configuration. + See [Security and Analysis Configuration](#security-and-analysis-configuration) below for details. + + Default is `null`. + + The `security_and_analysis` object accepts the following attributes: + + - [**`org_advanced_security`**](#attr-security_and_analysis-org_advanced_security: *(**Optional** `bool`)* + + If your GitHub Organization has split licensing for Advanced Security you can have Security and Analysis on non-public repositories. Otherwise Security and Analysis operates on all public repositories. Currently there is no known way to automatically detect this. + + Define `org_advanced_security` as true to give more visibility options. + + - [**`advanced_security`**](#attr-security_and_analysis-advanced_security): *(**Optional** `string`)* + + The advanced security configuration for the repository. See [Advanced Security Configuration](#advanced-security-configuration) below for details. + + Default is `"disabled"`, except if `org_advanced_security` is not true when it is `"enabled"`. + + - [**`secret_scanning`**](#attr-security_and_analysis-secret_scanning): *(**Optional** `string`)* + + The secret scanning configuration for the repository. See [Secret Scanning Configuration](#secret-scanning-configuration) below for details. + + Default is `"disabled"`. + + - [**`secret_scanning_push_protection`**](#attr-security_and_analysis-secret_scanning_push_protection): *(**Optional** `string`)* + + The secret scanning push protection configuration for the repository. See [Secret Scanning Push Protection Configuration](#secret-scanning-push-protection-configuration) below for details. + + Default is `"disabled"`. + ## External Documentation ### Terraform Github Provider Documentation diff --git a/locals.tf b/locals.tf deleted file mode 100644 index 2868abc..0000000 --- a/locals.tf +++ /dev/null @@ -1,5 +0,0 @@ -locals { - rulesets_by_name = { - for rs in var.rulesets : rs.name => rs - } -} diff --git a/main.tf b/main.tf index 022a731..ceb3dbf 100644 --- a/main.tf +++ b/main.tf @@ -153,6 +153,40 @@ resource "github_repository" "repository" { } } + dynamic "security_and_analysis" { + for_each = ( + var.visibility == "public" || local.security_and_analysis.org_advanced_security + ) ? [local.security_and_analysis] : [] + + content { + dynamic "advanced_security" { + for_each = var.visibility == "public" ? [] : [local.advanced_security_status] + content { + status = advanced_security.value + } + } + + secret_scanning { + status = local.security_and_analysis.secret_scanning + } + + secret_scanning_push_protection { + status = local.security_and_analysis.secret_scanning_push_protection + } + + # code_security, secret_scanning_ai_detection, and secret_scanning_non_provider_patterns require integrations/github >= 6.9 + # code_security { + # status = local.security_and_analysis.code_security + # } + # secret_scanning_ai_detection { + # status = local.security_and_analysis.secret_scanning_ai_detection + # } + # secret_scanning_non_provider_patterns { + # status = local.security_and_analysis.secret_scanning_non_provider_patterns + # } + } + } + lifecycle { ignore_changes = [ auto_init, @@ -160,6 +194,22 @@ resource "github_repository" "repository" { gitignore_template, template, ] + precondition { + condition = ( + ( + var.visibility == "public" + || local.security_and_analysis.org_advanced_security + || !local.saa_child_enabled + ) + && + local.push_protection_valid + ) + error_message = ( + local.push_protection_valid + ? "security_and_analysis cannot be used for private/internal repositories unless org_advanced_security is true." + : "secret_scanning_push_protection requires secret_scanning to also be enabled." + ) + } } } diff --git a/rulesets_resources.tf b/rulesets_resources.tf index cd5ab82..7a17d42 100644 --- a/rulesets_resources.tf +++ b/rulesets_resources.tf @@ -1,3 +1,9 @@ +locals { + rulesets_by_name = { + for rs in var.rulesets : rs.name => rs + } +} + resource "github_repository_ruleset" "ruleset" { for_each = local.rulesets_by_name diff --git a/securityanalysis_resources.tf b/securityanalysis_resources.tf new file mode 100644 index 0000000..c5fdc02 --- /dev/null +++ b/securityanalysis_resources.tf @@ -0,0 +1,29 @@ +locals { + security_and_analysis = var.security_and_analysis != null ? var.security_and_analysis : { + org_advanced_security = false + advanced_security = "disabled" + code_security = "disabled" + secret_scanning = "disabled" + secret_scanning_push_protection = "disabled" + secret_scanning_ai_detection = "disabled" + secret_scanning_non_provider_patterns = "disabled" + } + + saa_child_enabled = ( + local.security_and_analysis.code_security == "enabled" || + local.security_and_analysis.secret_scanning == "enabled" || + local.security_and_analysis.secret_scanning_push_protection == "enabled" || + local.security_and_analysis.secret_scanning_ai_detection == "enabled" || + local.security_and_analysis.secret_scanning_non_provider_patterns == "enabled" + ) + + advanced_security_status = ( + local.saa_child_enabled || local.security_and_analysis.advanced_security == "enabled" + ) ? "enabled" : "disabled" + + push_protection_valid = ( + local.security_and_analysis.secret_scanning_push_protection == "enabled" + ? local.security_and_analysis.secret_scanning == "enabled" + : true + ) +} diff --git a/securityanalysis_variables.tf b/securityanalysis_variables.tf new file mode 100644 index 0000000..656fe85 --- /dev/null +++ b/securityanalysis_variables.tf @@ -0,0 +1,31 @@ +variable "security_and_analysis" { + description = <