From 01d53d008d6f818174744ec0c391a7b7dcd74279 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:26:01 +1100 Subject: [PATCH 01/12] feat: Add github_organization_security_configuration and github_enterprise_security_configuration resources Adds two new resources to manage Code Security Configurations: - github_organization_security_configuration: manages code security configurations at the organization level - github_enterprise_security_configuration: manages code security configurations at the enterprise level Both resources include: - Full CRUD operations using GitHub's Code Security Configurations API - Composite IDs (org/enterprise + config ID) - 404-tolerant delete - tflog structured logging throughout - All optional fields use GetOk to avoid sending unset values - Custom import support - Shared expandCodeSecurityConfigurationCommon helper to avoid duplication - All 4 delegated fields on enterprise: code_scanning_delegated_alert_dismissal, secret_scanning_delegated_bypass, secret_scanning_delegated_bypass_options, secret_scanning_delegated_alert_dismissal - Fix flattenCodeScanningDefaultSetupOptions runner_type empty string drift Acceptance tests (5 per resource): - creates without error (with import verification) - updates without error - creates with nested options (runner, autosubmit) - creates with minimal config (with import verification) - creates with delegated bypass options Documentation added for both resources. Resolves #2412 Co-Authored-By: Claude Sonnet 4.6 --- github/provider.go | 2 + ...ithub_enterprise_security_configuration.go | 576 ++++++++++++++++++ ..._enterprise_security_configuration_test.go | 216 +++++++ ...hub_organization_security_configuration.go | 563 +++++++++++++++++ ...rganization_security_configuration_test.go | 217 +++++++ github/util_security_configuration.go | 184 ++++++ github/util_security_configuration_test.go | 59 ++ ...prise_security_configuration.html.markdown | 98 +++ 8 files changed, 1915 insertions(+) create mode 100644 github/resource_github_enterprise_security_configuration.go create mode 100644 github/resource_github_enterprise_security_configuration_test.go create mode 100644 github/resource_github_organization_security_configuration.go create mode 100644 github/resource_github_organization_security_configuration_test.go create mode 100644 github/util_security_configuration.go create mode 100644 github/util_security_configuration_test.go create mode 100644 website/docs/r/enterprise_security_configuration.html.markdown diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..f1c72082bf 100644 --- a/github/provider.go +++ b/github/provider.go @@ -180,6 +180,7 @@ func Provider() *schema.Provider { "github_organization_role_user": resourceGithubOrganizationRoleUser(), "github_organization_role_team_assignment": resourceGithubOrganizationRoleTeamAssignment(), "github_organization_ruleset": resourceGithubOrganizationRuleset(), + "github_organization_security_configuration": resourceGithubOrganizationSecurityConfiguration(), "github_organization_security_manager": resourceGithubOrganizationSecurityManager(), "github_organization_settings": resourceGithubOrganizationSettings(), "github_organization_webhook": resourceGithubOrganizationWebhook(), @@ -217,6 +218,7 @@ func Provider() *schema.Provider { "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), "github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(), "github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(), + "github_enterprise_security_configuration": resourceGithubEnterpriseSecurityConfiguration(), "github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(), }, diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go new file mode 100644 index 0000000000..669553cf79 --- /dev/null +++ b/github/resource_github_enterprise_security_configuration.go @@ -0,0 +1,576 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubEnterpriseSecurityConfiguration() *schema.Resource { + return &schema.Resource{ + Description: "Manages a code security configuration for a GitHub Enterprise.", + CreateContext: resourceGithubEnterpriseSecurityConfigurationCreate, + ReadContext: resourceGithubEnterpriseSecurityConfigurationRead, + UpdateContext: resourceGithubEnterpriseSecurityConfigurationUpdate, + DeleteContext: resourceGithubEnterpriseSecurityConfigurationDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubEnterpriseSecurityConfigurationImport, + }, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the code security configuration.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A description of the code security configuration.", + }, + "advanced_security": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The advanced security configuration for the code security configuration. Can be one of 'enabled', 'disabled'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", + }, false)), + }, + "dependency_graph": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependency graph configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "dependency_graph_autosubmit_action": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependency graph autosubmit action configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "dependency_graph_autosubmit_action_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The dependency graph autosubmit action options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "labeled_runners": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to use labeled runners for the dependency graph autosubmit action.", + }, + }, + }, + }, + "dependabot_alerts": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependabot alerts configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "dependabot_security_updates": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependabot security updates configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "code_scanning_default_setup": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The code scanning default setup configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "code_scanning_default_setup_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The code scanning default setup options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "runner_type": { + Type: schema.TypeString, + Optional: true, + Description: "The type of runner to use for code scanning default setup. Can be one of 'standard', 'labeled'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"standard", "labeled"}, false)), + }, + "runner_label": { + Type: schema.TypeString, + Optional: true, + Description: "The label of the runner to use for code scanning default setup.", + }, + }, + }, + }, + "code_scanning_delegated_alert_dismissal": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The code scanning delegated alert dismissal configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "code_scanning_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The code scanning options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allow_advanced": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to allow advanced security for code scanning.", + }, + }, + }, + }, + "code_security": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The code security configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_push_protection": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning push protection configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_delegated_bypass": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning delegated bypass configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_delegated_bypass_options": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "The secret scanning delegated bypass options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "reviewers": { + Type: schema.TypeList, + Optional: true, + Description: "The bypass reviewers for the secret scanning delegated bypass.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "reviewer_id": { + Type: schema.TypeInt, + Required: true, + Description: "The ID of the bypass reviewer.", + }, + "reviewer_type": { + Type: schema.TypeString, + Required: true, + Description: "The type of the bypass reviewer. Can be one of 'Team', 'Role'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"Team", "Role"}, false)), + }, + }, + }, + }, + }, + }, + }, + "secret_scanning_validity_checks": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning validity checks configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_non_provider_patterns": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning non provider patterns configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_generic_secrets": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning generic secrets configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_delegated_alert_dismissal": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning delegated alert dismissal configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_protection": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret protection configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "private_vulnerability_reporting": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The private vulnerability reporting configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "enforcement": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The enforcement configuration for the code security configuration. Can be one of 'enforced', 'unenforced'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enforced", "unenforced", + }, false)), + }, + "target_type": { + Type: schema.TypeString, + Computed: true, + Description: "The target type of the code security configuration.", + }, + }, + } +} + +func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterprise := d.Get("enterprise_slug").(string) + name := d.Get("name").(string) + + tflog.Debug(ctx, fmt.Sprintf("Creating enterprise code security configuration: %s/%s", enterprise, name), map[string]any{ + "enterprise": enterprise, + "name": name, + }) + + config := expandEnterpriseCodeSecurityConfiguration(d) + + configuration, _, err := client.Enterprise.CreateCodeSecurityConfiguration(ctx, enterprise, config) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to create enterprise code security configuration: %s/%s", enterprise, name), map[string]any{ + "enterprise": enterprise, + "name": name, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + id, err := buildID(enterprise, strconv.FormatInt(configuration.GetID(), 10)) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + tflog.Info(ctx, fmt.Sprintf("Created enterprise code security configuration: %s/%s (ID: %d)", enterprise, name, configuration.GetID()), map[string]any{ + "enterprise": enterprise, + "name": name, + "id": configuration.GetID(), + }) + + return resourceGithubEnterpriseSecurityConfigurationRead(ctx, d, meta) +} + +func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterprise, idStr, err := parseID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, fmt.Sprintf("Reading enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + configuration, _, err := client.Enterprise.GetCodeSecurityConfiguration(ctx, enterprise, id) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, fmt.Sprintf("Removing enterprise code security configuration %s/%d from state because it no longer exists in GitHub", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + d.SetId("") + return nil + } + } + tflog.Error(ctx, fmt.Sprintf("Failed to read enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + if err = d.Set("enterprise_slug", enterprise); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("name", configuration.Name); err != nil { + return diag.FromErr(err) + } + if err = d.Set("description", configuration.Description); err != nil { + return diag.FromErr(err) + } + if err = d.Set("advanced_security", configuration.GetAdvancedSecurity()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependency_graph", configuration.GetDependencyGraph()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependency_graph_autosubmit_action", configuration.GetDependencyGraphAutosubmitAction()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependency_graph_autosubmit_action_options", flattenDependencyGraphAutosubmitActionOptions(configuration.DependencyGraphAutosubmitActionOptions)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependabot_alerts", configuration.GetDependabotAlerts()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependabot_security_updates", configuration.GetDependabotSecurityUpdates()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("code_scanning_default_setup", configuration.GetCodeScanningDefaultSetup()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_default_setup_options", flattenCodeScanningDefaultSetupOptions(configuration.CodeScanningDefaultSetupOptions)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_options", flattenCodeScanningOptions(configuration.CodeScanningOptions)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("code_scanning_delegated_alert_dismissal", configuration.GetCodeScanningDelegatedAlertDismissal()); err != nil { + return diag.FromErr(err) + } + codeSec := configuration.GetCodeSecurity() + if codeSec == "" { + codeSec = "disabled" + } + if err = d.Set("code_security", codeSec); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning", configuration.GetSecretScanning()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_push_protection", configuration.GetSecretScanningPushProtection()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_validity_checks", configuration.GetSecretScanningValidityChecks()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_non_provider_patterns", configuration.GetSecretScanningNonProviderPatterns()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_generic_secrets", configuration.GetSecretScanningGenericSecrets()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_alert_dismissal", configuration.GetSecretScanningDelegatedAlertDismissal()); err != nil { + return diag.FromErr(err) + } + secretProt := configuration.GetSecretProtection() + if secretProt == "" { + secretProt = "disabled" + } + if err = d.Set("secret_protection", secretProt); err != nil { + return diag.FromErr(err) + } + if err = d.Set("private_vulnerability_reporting", configuration.GetPrivateVulnerabilityReporting()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("enforcement", configuration.GetEnforcement()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("target_type", configuration.GetTargetType()); err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, fmt.Sprintf("Successfully read enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + return nil +} + +func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterprise, idStr, err := parseID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + tflog.Debug(ctx, fmt.Sprintf("Updating enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + config := expandEnterpriseCodeSecurityConfiguration(d) + + _, _, err = client.Enterprise.UpdateCodeSecurityConfiguration(ctx, enterprise, id, config) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to update enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + tflog.Info(ctx, fmt.Sprintf("Updated enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + return resourceGithubEnterpriseSecurityConfigurationRead(ctx, d, meta) +} + +func resourceGithubEnterpriseSecurityConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + enterprise, idStr, err := parseID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + tflog.Debug(ctx, fmt.Sprintf("Deleting enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + _, err = client.Enterprise.DeleteCodeSecurityConfiguration(ctx, enterprise, id) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, fmt.Sprintf("Enterprise code security configuration %s/%d already deleted", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + return nil + } + tflog.Error(ctx, fmt.Sprintf("Failed to delete enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + tflog.Info(ctx, fmt.Sprintf("Deleted enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + return nil +} + +func resourceGithubEnterpriseSecurityConfigurationImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + enterpriseSlug, configID, err := parseID2(d.Id()) + if err != nil { + return nil, fmt.Errorf("invalid import specified: supplied import must be written as :. Parse error: %w", err) + } + + id, err := buildID(enterpriseSlug, configID) + if err != nil { + return nil, err + } + d.SetId(id) + + if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func expandEnterpriseCodeSecurityConfiguration(d *schema.ResourceData) github.CodeSecurityConfiguration { + return expandCodeSecurityConfigurationCommon(d) +} diff --git a/github/resource_github_enterprise_security_configuration_test.go b/github/resource_github_enterprise_security_configuration_test.go new file mode 100644 index 0000000000..5f45863a5f --- /dev/null +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -0,0 +1,216 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { + t.Run("creates enterprise security configuration without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-%s", randomID) + + config := fmt.Sprintf(` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "Test configuration" + advanced_security = "enabled" + dependency_graph = "enabled" + dependabot_alerts = "enabled" + dependabot_security_updates = "enabled" + code_scanning_default_setup = "enabled" + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + private_vulnerability_reporting = "enabled" + enforcement = "enforced" + }`, testAccConf.enterpriseSlug, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("description"), knownvalue.StringExact("Test configuration")), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("advanced_security"), knownvalue.StringExact("enabled")), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("enforcement"), knownvalue.StringExact("enforced")), + }, + }, + { + ResourceName: "github_enterprise_security_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("updates enterprise security configuration without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-%s", randomID) + configNameUpdated := fmt.Sprintf("test-config-updated-%s", randomID) + + tmpl := ` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "%s" + advanced_security = "%s" + }` + configBefore := fmt.Sprintf(tmpl, testAccConf.enterpriseSlug, configName, "Test configuration", "disabled") + configAfter := fmt.Sprintf(tmpl, testAccConf.enterpriseSlug, configNameUpdated, "Test configuration updated", "enabled") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configBefore, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("advanced_security"), knownvalue.StringExact("disabled")), + }, + }, + { + Config: configAfter, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configNameUpdated)), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("advanced_security"), knownvalue.StringExact("enabled")), + }, + }, + }, + }) + }) + + t.Run("creates enterprise security configuration with options", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-options-%s", randomID) + + config := fmt.Sprintf(` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "Test configuration with options" + advanced_security = "enabled" + dependency_graph = "enabled" + dependency_graph_autosubmit_action = "enabled" + dependency_graph_autosubmit_action_options { + labeled_runners = true + } + code_scanning_default_setup = "enabled" + code_scanning_default_setup_options { + runner_type = "labeled" + runner_label = "code-scanning" + } + secret_scanning = "enabled" + }`, testAccConf.enterpriseSlug, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("dependency_graph_autosubmit_action_options").AtSliceIndex(0).AtMapKey("labeled_runners"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("code_scanning_default_setup_options").AtSliceIndex(0).AtMapKey("runner_type"), knownvalue.StringExact("labeled")), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("code_scanning_default_setup_options").AtSliceIndex(0).AtMapKey("runner_label"), knownvalue.StringExact("code-scanning")), + }, + }, + { + ResourceName: "github_enterprise_security_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("creates enterprise security configuration with minimal config", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-minimal-%s", randomID) + + config := fmt.Sprintf(` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "Minimal test configuration" + }`, testAccConf.enterpriseSlug, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("target_type"), knownvalue.NotNull()), + }, + }, + }, + }) + }) + + t.Run("creates enterprise security configuration with delegated bypass options", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-bypass-%s", randomID) + + config := fmt.Sprintf(` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "Test configuration with delegated bypass" + advanced_security = "enabled" + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + secret_scanning_delegated_bypass = "enabled" + secret_scanning_delegated_bypass_options { + reviewers { + reviewer_id = 1 + reviewer_type = "Team" + } + } + }`, testAccConf.enterpriseSlug, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("secret_scanning_delegated_bypass"), knownvalue.StringExact("enabled")), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("secret_scanning_delegated_bypass_options").AtSliceIndex(0).AtMapKey("reviewers").AtSliceIndex(0).AtMapKey("reviewer_type"), knownvalue.StringExact("Team")), + }, + }, + }, + }) + }) +} diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go new file mode 100644 index 0000000000..80c0684951 --- /dev/null +++ b/github/resource_github_organization_security_configuration.go @@ -0,0 +1,563 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubOrganizationSecurityConfiguration() *schema.Resource { + return &schema.Resource{ + Description: "Manages a code security configuration for a GitHub Organization.", + CreateContext: resourceGithubOrganizationSecurityConfigurationCreate, + ReadContext: resourceGithubOrganizationSecurityConfigurationRead, + UpdateContext: resourceGithubOrganizationSecurityConfigurationUpdate, + DeleteContext: resourceGithubOrganizationSecurityConfigurationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the code security configuration.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A description of the code security configuration.", + }, + "advanced_security": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The advanced security configuration for the code security configuration. Can be one of 'enabled', 'disabled'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", + }, false)), + }, + "dependency_graph": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependency graph configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "dependency_graph_autosubmit_action": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependency graph autosubmit action configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "dependency_graph_autosubmit_action_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The dependency graph autosubmit action options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "labeled_runners": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to use labeled runners for the dependency graph autosubmit action.", + }, + }, + }, + }, + "dependabot_alerts": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependabot alerts configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "dependabot_security_updates": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The dependabot security updates configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "code_scanning_default_setup": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The code scanning default setup configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "code_scanning_default_setup_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The code scanning default setup options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "runner_type": { + Type: schema.TypeString, + Optional: true, + Description: "The type of runner to use for code scanning default setup. Can be one of 'standard', 'labeled'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"standard", "labeled"}, false)), + }, + "runner_label": { + Type: schema.TypeString, + Optional: true, + Description: "The label of the runner to use for code scanning default setup.", + }, + }, + }, + }, + "code_scanning_delegated_alert_dismissal": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The code scanning delegated alert dismissal configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "code_scanning_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The code scanning options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allow_advanced": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to allow advanced security for code scanning.", + }, + }, + }, + }, + "code_security": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The code security configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_push_protection": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning push protection configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_delegated_bypass": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning delegated bypass configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_delegated_bypass_options": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "The secret scanning delegated bypass options for the code security configuration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "reviewers": { + Type: schema.TypeList, + Optional: true, + Description: "The bypass reviewers for the secret scanning delegated bypass.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "reviewer_id": { + Type: schema.TypeInt, + Required: true, + Description: "The ID of the bypass reviewer.", + }, + "reviewer_type": { + Type: schema.TypeString, + Required: true, + Description: "The type of the bypass reviewer. Can be one of 'Team', 'Role'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"Team", "Role"}, false)), + }, + }, + }, + }, + }, + }, + }, + "secret_scanning_validity_checks": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning validity checks configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_non_provider_patterns": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning non provider patterns configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_generic_secrets": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning generic secrets configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_scanning_delegated_alert_dismissal": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret scanning delegated alert dismissal configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "secret_protection": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The secret protection configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "private_vulnerability_reporting": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The private vulnerability reporting configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enabled", "disabled", "not_set", + }, false)), + }, + "enforcement": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The enforcement configuration for the code security configuration. Can be one of 'enforced', 'unenforced'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "enforced", "unenforced", + }, false)), + }, + "target_type": { + Type: schema.TypeString, + Computed: true, + Description: "The target type of the code security configuration.", + }, + }, + } +} + +func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + err := checkOrganization(meta) + if err != nil { + return diag.FromErr(err) + } + client := meta.(*Owner).v3client + org := meta.(*Owner).name + name := d.Get("name").(string) + + tflog.Debug(ctx, fmt.Sprintf("Creating organization code security configuration: %s/%s", org, name), map[string]any{ + "organization": org, + "name": name, + }) + + config := expandCodeSecurityConfiguration(d) + + configuration, _, err := client.Organizations.CreateCodeSecurityConfiguration(ctx, org, config) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to create organization code security configuration: %s/%s", org, name), map[string]any{ + "organization": org, + "name": name, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + id, err := buildID(org, strconv.FormatInt(configuration.GetID(), 10)) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + tflog.Info(ctx, fmt.Sprintf("Created organization code security configuration: %s/%s (ID: %d)", org, name, configuration.GetID()), map[string]any{ + "organization": org, + "name": name, + "id": configuration.GetID(), + }) + + return resourceGithubOrganizationSecurityConfigurationRead(ctx, d, meta) +} + +func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + err := checkOrganization(meta) + if err != nil { + return diag.FromErr(err) + } + client := meta.(*Owner).v3client + + org, idStr, err := parseID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, fmt.Sprintf("Reading organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + }) + + configuration, _, err := client.Organizations.GetCodeSecurityConfiguration(ctx, org, id) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, fmt.Sprintf("Removing organization code security configuration %s/%d from state because it no longer exists in GitHub", org, id), map[string]any{ + "organization": org, + "id": id, + }) + d.SetId("") + return nil + } + } + tflog.Error(ctx, fmt.Sprintf("Failed to read organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + if err = d.Set("name", configuration.Name); err != nil { + return diag.FromErr(err) + } + if err = d.Set("description", configuration.Description); err != nil { + return diag.FromErr(err) + } + if err = d.Set("advanced_security", configuration.GetAdvancedSecurity()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependency_graph", configuration.GetDependencyGraph()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependency_graph_autosubmit_action", configuration.GetDependencyGraphAutosubmitAction()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependency_graph_autosubmit_action_options", flattenDependencyGraphAutosubmitActionOptions(configuration.DependencyGraphAutosubmitActionOptions)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependabot_alerts", configuration.GetDependabotAlerts()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dependabot_security_updates", configuration.GetDependabotSecurityUpdates()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("code_scanning_default_setup", configuration.GetCodeScanningDefaultSetup()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_default_setup_options", flattenCodeScanningDefaultSetupOptions(configuration.CodeScanningDefaultSetupOptions)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("code_scanning_delegated_alert_dismissal", configuration.GetCodeScanningDelegatedAlertDismissal()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_options", flattenCodeScanningOptions(configuration.CodeScanningOptions)); err != nil { + return diag.FromErr(err) + } + codeSec := configuration.GetCodeSecurity() + if codeSec == "" { + codeSec = "disabled" + } + if err = d.Set("code_security", codeSec); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning", configuration.GetSecretScanning()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_push_protection", configuration.GetSecretScanningPushProtection()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_validity_checks", configuration.GetSecretScanningValidityChecks()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_non_provider_patterns", configuration.GetSecretScanningNonProviderPatterns()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_generic_secrets", configuration.GetSecretScanningGenericSecrets()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_alert_dismissal", configuration.GetSecretScanningDelegatedAlertDismissal()); err != nil { + return diag.FromErr(err) + } + secretProt := configuration.GetSecretProtection() + if secretProt == "" { + secretProt = "disabled" + } + if err = d.Set("secret_protection", secretProt); err != nil { + return diag.FromErr(err) + } + if err = d.Set("private_vulnerability_reporting", configuration.GetPrivateVulnerabilityReporting()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("enforcement", configuration.GetEnforcement()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("target_type", configuration.GetTargetType()); err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, fmt.Sprintf("Successfully read organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + }) + + return nil +} + +func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + err := checkOrganization(meta) + if err != nil { + return diag.FromErr(err) + } + client := meta.(*Owner).v3client + + org, idStr, err := parseID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + tflog.Debug(ctx, fmt.Sprintf("Updating organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + }) + + config := expandCodeSecurityConfiguration(d) + + _, _, err = client.Organizations.UpdateCodeSecurityConfiguration(ctx, org, id, config) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to update organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + tflog.Info(ctx, fmt.Sprintf("Updated organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + }) + + return resourceGithubOrganizationSecurityConfigurationRead(ctx, d, meta) +} + +func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + err := checkOrganization(meta) + if err != nil { + return diag.FromErr(err) + } + client := meta.(*Owner).v3client + + org, idStr, err := parseID2(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + tflog.Debug(ctx, fmt.Sprintf("Deleting organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + }) + + _, err = client.Organizations.DeleteCodeSecurityConfiguration(ctx, org, id) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, fmt.Sprintf("Organization code security configuration %s/%d already deleted", org, id), map[string]any{ + "organization": org, + "id": id, + }) + return nil + } + tflog.Error(ctx, fmt.Sprintf("Failed to delete organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + tflog.Info(ctx, fmt.Sprintf("Deleted organization code security configuration: %s/%d", org, id), map[string]any{ + "organization": org, + "id": id, + }) + + return nil +} + +func expandCodeSecurityConfiguration(d *schema.ResourceData) github.CodeSecurityConfiguration { + return expandCodeSecurityConfigurationCommon(d) +} diff --git a/github/resource_github_organization_security_configuration_test.go b/github/resource_github_organization_security_configuration_test.go new file mode 100644 index 0000000000..65ced074b8 --- /dev/null +++ b/github/resource_github_organization_security_configuration_test.go @@ -0,0 +1,217 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { + t.Run("creates organization security configuration without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "Test configuration" + advanced_security = "enabled" + dependency_graph = "enabled" + dependabot_alerts = "enabled" + dependabot_security_updates = "enabled" + code_scanning_default_setup = "enabled" + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + private_vulnerability_reporting = "enabled" + enforcement = "enforced" + }`, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("description"), knownvalue.StringExact("Test configuration")), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("advanced_security"), knownvalue.StringExact("enabled")), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("enforcement"), knownvalue.StringExact("enforced")), + }, + }, + { + ResourceName: "github_organization_security_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("updates organization security configuration without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-%s", randomID) + configNameUpdated := fmt.Sprintf("test-config-updated-%s", randomID) + + tmpl := ` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "%s" + advanced_security = "%s" + }` + configBefore := fmt.Sprintf(tmpl, configName, "Test configuration", "disabled") + configAfter := fmt.Sprintf(tmpl, configNameUpdated, "Test configuration updated", "enabled") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configBefore, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("advanced_security"), knownvalue.StringExact("disabled")), + }, + }, + { + Config: configAfter, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configNameUpdated)), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("advanced_security"), knownvalue.StringExact("enabled")), + }, + }, + }, + }) + }) + + t.Run("creates organization security configuration with options", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-options-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "Test configuration with options" + advanced_security = "enabled" + dependency_graph = "enabled" + dependency_graph_autosubmit_action = "enabled" + dependency_graph_autosubmit_action_options { + labeled_runners = true + } + code_scanning_default_setup = "enabled" + code_scanning_default_setup_options { + runner_type = "labeled" + runner_label = "code-scanning" + } + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + }`, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("dependency_graph_autosubmit_action_options").AtSliceIndex(0).AtMapKey("labeled_runners"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("code_scanning_default_setup_options").AtSliceIndex(0).AtMapKey("runner_type"), knownvalue.StringExact("labeled")), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("code_scanning_default_setup_options").AtSliceIndex(0).AtMapKey("runner_label"), knownvalue.StringExact("code-scanning")), + }, + }, + { + ResourceName: "github_organization_security_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("creates organization security configuration with minimal config", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-minimal-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "Minimal test configuration" + }`, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("name"), knownvalue.StringExact(configName)), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("target_type"), knownvalue.NotNull()), + }, + }, + { + ResourceName: "github_organization_security_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("creates organization security configuration with delegated bypass options", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("test-config-bypass-%s", randomID) + + config := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "Test configuration with delegated bypass" + advanced_security = "enabled" + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + secret_scanning_delegated_bypass = "enabled" + secret_scanning_delegated_bypass_options { + reviewers { + reviewer_id = 1 + reviewer_type = "Team" + } + } + }`, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("secret_scanning_delegated_bypass"), knownvalue.StringExact("enabled")), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("secret_scanning_delegated_bypass_options").AtSliceIndex(0).AtMapKey("reviewers").AtSliceIndex(0).AtMapKey("reviewer_type"), knownvalue.StringExact("Team")), + }, + }, + }, + }) + }) +} diff --git a/github/util_security_configuration.go b/github/util_security_configuration.go new file mode 100644 index 0000000000..3bdb486bb1 --- /dev/null +++ b/github/util_security_configuration.go @@ -0,0 +1,184 @@ +package github + +import ( + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// flattenDependencyGraphAutosubmitActionOptions converts DependencyGraphAutosubmitActionOptions to a Terraform-compatible format +func flattenDependencyGraphAutosubmitActionOptions(options *github.DependencyGraphAutosubmitActionOptions) []any { + if options == nil { + return []any{} + } + autosubmitOpts := make(map[string]any) + if options.LabeledRunners != nil { + autosubmitOpts["labeled_runners"] = options.GetLabeledRunners() + } + return []any{autosubmitOpts} +} + +// flattenCodeScanningDefaultSetupOptions converts CodeScanningDefaultSetupOptions to a Terraform-compatible format +func flattenCodeScanningDefaultSetupOptions(options *github.CodeScanningDefaultSetupOptions) []any { + if options == nil { + return []any{} + } + setupOpts := make(map[string]any) + if options.RunnerType != "" { + setupOpts["runner_type"] = options.RunnerType + } + if options.RunnerLabel != nil { + setupOpts["runner_label"] = options.GetRunnerLabel() + } + return []any{setupOpts} +} + +// flattenCodeScanningOptions converts CodeScanningOptions to a Terraform-compatible format +func flattenCodeScanningOptions(options *github.CodeScanningOptions) []any { + if options == nil { + return []any{} + } + scanOpts := make(map[string]any) + if options.AllowAdvanced != nil { + scanOpts["allow_advanced"] = options.GetAllowAdvanced() + } + return []any{scanOpts} +} + +// flattenSecretScanningDelegatedBypassOptions converts SecretScanningDelegatedBypassOptions to a Terraform-compatible format +func flattenSecretScanningDelegatedBypassOptions(options *github.SecretScanningDelegatedBypassOptions) []any { + if options == nil { + return []any{} + } + bypassOpts := make(map[string]any) + if options.Reviewers != nil { + reviewers := make([]any, 0, len(options.Reviewers)) + for _, reviewer := range options.Reviewers { + reviewerMap := make(map[string]any) + reviewerMap["reviewer_id"] = reviewer.ReviewerID + reviewerMap["reviewer_type"] = reviewer.ReviewerType + reviewers = append(reviewers, reviewerMap) + } + bypassOpts["reviewers"] = reviewers + } + return []any{bypassOpts} +} + +// expandCodeSecurityConfigurationCommon builds a CodeSecurityConfiguration from Terraform resource data. +// Used by both the organization and enterprise security configuration resources. +func expandCodeSecurityConfigurationCommon(d *schema.ResourceData) github.CodeSecurityConfiguration { + config := github.CodeSecurityConfiguration{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + } + + if val, ok := d.GetOk("advanced_security"); ok { + config.AdvancedSecurity = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("dependency_graph"); ok { + config.DependencyGraph = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("dependency_graph_autosubmit_action"); ok { + config.DependencyGraphAutosubmitAction = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("dependabot_alerts"); ok { + config.DependabotAlerts = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("dependabot_security_updates"); ok { + config.DependabotSecurityUpdates = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("code_scanning_default_setup"); ok { + config.CodeScanningDefaultSetup = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("code_scanning_delegated_alert_dismissal"); ok { + config.CodeScanningDelegatedAlertDismissal = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("code_security"); ok { + config.CodeSecurity = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning"); ok { + config.SecretScanning = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning_push_protection"); ok { + config.SecretScanningPushProtection = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning_delegated_bypass"); ok { + config.SecretScanningDelegatedBypass = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning_validity_checks"); ok { + config.SecretScanningValidityChecks = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning_non_provider_patterns"); ok { + config.SecretScanningNonProviderPatterns = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning_generic_secrets"); ok { + config.SecretScanningGenericSecrets = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_scanning_delegated_alert_dismissal"); ok { + config.SecretScanningDelegatedAlertDismissal = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("secret_protection"); ok { + config.SecretProtection = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("private_vulnerability_reporting"); ok { + config.PrivateVulnerabilityReporting = github.Ptr(val.(string)) + } + if val, ok := d.GetOk("enforcement"); ok { + config.Enforcement = github.Ptr(val.(string)) + } + + if val, ok := d.GetOk("dependency_graph_autosubmit_action_options"); ok { + optionsList := val.([]any) + if len(optionsList) > 0 { + autosubmitOpts := optionsList[0].(map[string]any) + config.DependencyGraphAutosubmitActionOptions = &github.DependencyGraphAutosubmitActionOptions{ + LabeledRunners: github.Ptr(autosubmitOpts["labeled_runners"].(bool)), + } + } + } + + if val, ok := d.GetOk("code_scanning_default_setup_options"); ok { + optionsList := val.([]any) + if len(optionsList) > 0 { + setupOpts := optionsList[0].(map[string]any) + config.CodeScanningDefaultSetupOptions = &github.CodeScanningDefaultSetupOptions{ + RunnerType: setupOpts["runner_type"].(string), + } + if runnerLabel, ok := setupOpts["runner_label"].(string); ok && runnerLabel != "" { + config.CodeScanningDefaultSetupOptions.RunnerLabel = github.Ptr(runnerLabel) + } + } + } + + if val, ok := d.GetOk("code_scanning_options"); ok { + optionsList := val.([]any) + if len(optionsList) > 0 { + scanOpts := optionsList[0].(map[string]any) + config.CodeScanningOptions = &github.CodeScanningOptions{ + AllowAdvanced: github.Ptr(scanOpts["allow_advanced"].(bool)), + } + } + } + + if val, ok := d.GetOk("secret_scanning_delegated_bypass_options"); ok { + optionsList := val.([]any) + if len(optionsList) > 0 { + bypassOpts := optionsList[0].(map[string]any) + options := &github.SecretScanningDelegatedBypassOptions{} + if reviewersVal, ok := bypassOpts["reviewers"]; ok { + reviewersList := reviewersVal.([]any) + reviewers := make([]*github.BypassReviewer, 0, len(reviewersList)) + for _, reviewerRaw := range reviewersList { + reviewerMap := reviewerRaw.(map[string]any) + reviewers = append(reviewers, &github.BypassReviewer{ + ReviewerID: int64(reviewerMap["reviewer_id"].(int)), + ReviewerType: reviewerMap["reviewer_type"].(string), + }) + } + options.Reviewers = reviewers + } + config.SecretScanningDelegatedBypassOptions = options + } + } + + return config +} diff --git a/github/util_security_configuration_test.go b/github/util_security_configuration_test.go new file mode 100644 index 0000000000..db1a703d16 --- /dev/null +++ b/github/util_security_configuration_test.go @@ -0,0 +1,59 @@ +package github + +import ( + "testing" + + "github.com/google/go-github/v83/github" +) + +func TestFlattenCodeScanningDefaultSetupOptions(t *testing.T) { + t.Run("returns empty slice when options is nil", func(t *testing.T) { + result := flattenCodeScanningDefaultSetupOptions(nil) + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }) + + t.Run("omits runner_type key when RunnerType is empty string", func(t *testing.T) { + opts := &github.CodeScanningDefaultSetupOptions{ + RunnerType: "", + } + result := flattenCodeScanningDefaultSetupOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["runner_type"]; ok { + t.Errorf("runner_type should be absent when RunnerType is empty, got %q", m["runner_type"]) + } + }) + + t.Run("sets runner_type when RunnerType is non-empty", func(t *testing.T) { + opts := &github.CodeScanningDefaultSetupOptions{ + RunnerType: "standard", + } + result := flattenCodeScanningDefaultSetupOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["runner_type"] != "standard" { + t.Errorf("expected runner_type %q, got %q", "standard", m["runner_type"]) + } + }) + + t.Run("sets runner_label when RunnerLabel is non-nil", func(t *testing.T) { + opts := &github.CodeScanningDefaultSetupOptions{ + RunnerType: "labeled", + RunnerLabel: github.Ptr("my-runner"), + } + result := flattenCodeScanningDefaultSetupOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["runner_label"] != "my-runner" { + t.Errorf("expected runner_label %q, got %q", "my-runner", m["runner_label"]) + } + }) +} diff --git a/website/docs/r/enterprise_security_configuration.html.markdown b/website/docs/r/enterprise_security_configuration.html.markdown new file mode 100644 index 0000000000..8aaa4edfdc --- /dev/null +++ b/website/docs/r/enterprise_security_configuration.html.markdown @@ -0,0 +1,98 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_security_configuration" +description: |- + Manages a code security configuration for a GitHub Enterprise. +--- + +# github_enterprise_security_configuration + +This resource allows you to create and manage code security configurations for a GitHub Enterprise. + +## Example Usage + +```hcl +resource "github_enterprise_security_configuration" "default" { + enterprise_slug = "my-enterprise" + name = "default-config" + description = "Default security configuration" + advanced_security = "enabled" + dependency_graph = "enabled" + dependabot_alerts = "enabled" + dependabot_security_updates = "enabled" + code_scanning_default_setup = "enabled" + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + private_vulnerability_reporting = "enabled" + enforcement = "enforced" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. Changing this forces a new resource to be created. +* `name` - (Required) The name of the code security configuration. +* `description` - (Required) A description of the code security configuration. +* `advanced_security` - (Optional) The advanced security configuration. Can be one of `enabled`, `disabled`. +* `dependency_graph` - (Optional) The dependency graph configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `dependency_graph_autosubmit_action` - (Optional) The dependency graph autosubmit action configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `dependency_graph_autosubmit_action_options` - (Optional) The dependency graph autosubmit action options. See [Dependency Graph Autosubmit Action Options](#dependency-graph-autosubmit-action-options) below for details. +* `dependabot_alerts` - (Optional) The dependabot alerts configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `dependabot_security_updates` - (Optional) The dependabot security updates configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `code_scanning_default_setup` - (Optional) The code scanning default setup configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `code_scanning_default_setup_options` - (Optional) The code scanning default setup options. See [Code Scanning Default Setup Options](#code-scanning-default-setup-options) below for details. +* `code_scanning_delegated_alert_dismissal` - (Optional) The code scanning delegated alert dismissal configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `code_scanning_options` - (Optional) The code scanning options. See [Code Scanning Options](#code-scanning-options) below for details. +* `code_security` - (Optional) The code security configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning` - (Optional) The secret scanning configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_push_protection` - (Optional) The secret scanning push protection configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_delegated_bypass` - (Optional) The secret scanning delegated bypass configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_delegated_bypass_options` - (Optional) The secret scanning delegated bypass options. See [Secret Scanning Delegated Bypass Options](#secret-scanning-delegated-bypass-options) below for details. +* `secret_scanning_validity_checks` - (Optional) The secret scanning validity checks configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_non_provider_patterns` - (Optional) The secret scanning non provider patterns configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_generic_secrets` - (Optional) The secret scanning generic secrets configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_delegated_alert_dismissal` - (Optional) The secret scanning delegated alert dismissal configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_protection` - (Optional) The secret protection configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `private_vulnerability_reporting` - (Optional) The private vulnerability reporting configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `enforcement` - (Optional) The enforcement configuration. Can be one of `enforced`, `unenforced`. + +## Attributes Reference + +* `target_type` - The target type of the code security configuration. + +### Dependency Graph Autosubmit Action Options + +The `dependency_graph_autosubmit_action_options` block supports: + +* `labeled_runners` - (Optional) Whether to use labeled runners for the dependency graph autosubmit action. + +### Code Scanning Default Setup Options + +The `code_scanning_default_setup_options` block supports: + +* `runner_type` - (Optional) The type of runner to use for code scanning default setup. Can be one of `standard`, `labeled`. +* `runner_label` - (Optional) The label of the runner to use for code scanning default setup. + +### Code Scanning Options + +The `code_scanning_options` block supports: + +* `allow_advanced` - (Optional) Whether to allow advanced security for code scanning. + +### Secret Scanning Delegated Bypass Options + +The `secret_scanning_delegated_bypass_options` block supports: + +* `reviewers` - (Optional) The bypass reviewers. Each entry supports: + * `reviewer_id` - (Required) The ID of the bypass reviewer (team or role ID). + * `reviewer_type` - (Required) The type of the bypass reviewer. Can be one of `Team`, `Role`. + +## Import + +GitHub Enterprise Code Security Configurations can be imported using the enterprise slug and the configuration ID separated by a colon, e.g. + +```text +$ terraform import github_enterprise_security_configuration.example my-enterprise:123 +``` From 49d5cbf9f9a1312c617a4dc388b9c42f6fc4f0f6 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:38:25 +1100 Subject: [PATCH 02/12] refactor: extract shared setState helper, remove one-liner expand wrappers - Add setCodeSecurityConfigurationState() to util_security_configuration.go, replacing ~83 identical d.Set() lines duplicated across both Read functions - Remove expandCodeSecurityConfiguration() and expandEnterpriseCodeSecurityConfiguration() one-liner wrappers; callers now call expandCodeSecurityConfigurationCommon() directly Co-Authored-By: Claude Sonnet 4.6 --- ...ithub_enterprise_security_configuration.go | 91 +------------------ ...hub_organization_security_configuration.go | 91 +------------------ github/util_security_configuration.go | 90 ++++++++++++++++++ 3 files changed, 98 insertions(+), 174 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 669553cf79..079ace0c9f 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -308,7 +308,7 @@ func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d "name": name, }) - config := expandEnterpriseCodeSecurityConfiguration(d) + config := expandCodeSecurityConfigurationCommon(d) configuration, _, err := client.Enterprise.CreateCodeSecurityConfiguration(ctx, enterprise, config) if err != nil { @@ -378,88 +378,8 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s return diag.FromErr(err) } - if err = d.Set("name", configuration.Name); err != nil { - return diag.FromErr(err) - } - if err = d.Set("description", configuration.Description); err != nil { - return diag.FromErr(err) - } - if err = d.Set("advanced_security", configuration.GetAdvancedSecurity()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependency_graph", configuration.GetDependencyGraph()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependency_graph_autosubmit_action", configuration.GetDependencyGraphAutosubmitAction()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("dependency_graph_autosubmit_action_options", flattenDependencyGraphAutosubmitActionOptions(configuration.DependencyGraphAutosubmitActionOptions)); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependabot_alerts", configuration.GetDependabotAlerts()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependabot_security_updates", configuration.GetDependabotSecurityUpdates()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("code_scanning_default_setup", configuration.GetCodeScanningDefaultSetup()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("code_scanning_default_setup_options", flattenCodeScanningDefaultSetupOptions(configuration.CodeScanningDefaultSetupOptions)); err != nil { - return diag.FromErr(err) - } - if err := d.Set("code_scanning_options", flattenCodeScanningOptions(configuration.CodeScanningOptions)); err != nil { - return diag.FromErr(err) - } - if err = d.Set("code_scanning_delegated_alert_dismissal", configuration.GetCodeScanningDelegatedAlertDismissal()); err != nil { - return diag.FromErr(err) - } - codeSec := configuration.GetCodeSecurity() - if codeSec == "" { - codeSec = "disabled" - } - if err = d.Set("code_security", codeSec); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning", configuration.GetSecretScanning()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_push_protection", configuration.GetSecretScanningPushProtection()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_validity_checks", configuration.GetSecretScanningValidityChecks()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_non_provider_patterns", configuration.GetSecretScanningNonProviderPatterns()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_generic_secrets", configuration.GetSecretScanningGenericSecrets()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_delegated_alert_dismissal", configuration.GetSecretScanningDelegatedAlertDismissal()); err != nil { - return diag.FromErr(err) - } - secretProt := configuration.GetSecretProtection() - if secretProt == "" { - secretProt = "disabled" - } - if err = d.Set("secret_protection", secretProt); err != nil { - return diag.FromErr(err) - } - if err = d.Set("private_vulnerability_reporting", configuration.GetPrivateVulnerabilityReporting()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("enforcement", configuration.GetEnforcement()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("target_type", configuration.GetTargetType()); err != nil { - return diag.FromErr(err) + if diags := setCodeSecurityConfigurationState(d, configuration); diags != nil { + return diags } tflog.Trace(ctx, fmt.Sprintf("Successfully read enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ @@ -488,7 +408,7 @@ func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d "id": id, }) - config := expandEnterpriseCodeSecurityConfiguration(d) + config := expandCodeSecurityConfigurationCommon(d) _, _, err = client.Enterprise.UpdateCodeSecurityConfiguration(ctx, enterprise, id, config) if err != nil { @@ -571,6 +491,3 @@ func resourceGithubEnterpriseSecurityConfigurationImport(ctx context.Context, d return []*schema.ResourceData{d}, nil } -func expandEnterpriseCodeSecurityConfiguration(d *schema.ResourceData) github.CodeSecurityConfiguration { - return expandCodeSecurityConfigurationCommon(d) -} diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index 80c0684951..b407aa6469 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -306,7 +306,7 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, "name": name, }) - config := expandCodeSecurityConfiguration(d) + config := expandCodeSecurityConfigurationCommon(d) configuration, _, err := client.Organizations.CreateCodeSecurityConfiguration(ctx, org, config) if err != nil { @@ -376,88 +376,8 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d return diag.FromErr(err) } - if err = d.Set("name", configuration.Name); err != nil { - return diag.FromErr(err) - } - if err = d.Set("description", configuration.Description); err != nil { - return diag.FromErr(err) - } - if err = d.Set("advanced_security", configuration.GetAdvancedSecurity()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependency_graph", configuration.GetDependencyGraph()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependency_graph_autosubmit_action", configuration.GetDependencyGraphAutosubmitAction()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("dependency_graph_autosubmit_action_options", flattenDependencyGraphAutosubmitActionOptions(configuration.DependencyGraphAutosubmitActionOptions)); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependabot_alerts", configuration.GetDependabotAlerts()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("dependabot_security_updates", configuration.GetDependabotSecurityUpdates()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("code_scanning_default_setup", configuration.GetCodeScanningDefaultSetup()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("code_scanning_default_setup_options", flattenCodeScanningDefaultSetupOptions(configuration.CodeScanningDefaultSetupOptions)); err != nil { - return diag.FromErr(err) - } - if err = d.Set("code_scanning_delegated_alert_dismissal", configuration.GetCodeScanningDelegatedAlertDismissal()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("code_scanning_options", flattenCodeScanningOptions(configuration.CodeScanningOptions)); err != nil { - return diag.FromErr(err) - } - codeSec := configuration.GetCodeSecurity() - if codeSec == "" { - codeSec = "disabled" - } - if err = d.Set("code_security", codeSec); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning", configuration.GetSecretScanning()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_push_protection", configuration.GetSecretScanningPushProtection()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_validity_checks", configuration.GetSecretScanningValidityChecks()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_non_provider_patterns", configuration.GetSecretScanningNonProviderPatterns()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_generic_secrets", configuration.GetSecretScanningGenericSecrets()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("secret_scanning_delegated_alert_dismissal", configuration.GetSecretScanningDelegatedAlertDismissal()); err != nil { - return diag.FromErr(err) - } - secretProt := configuration.GetSecretProtection() - if secretProt == "" { - secretProt = "disabled" - } - if err = d.Set("secret_protection", secretProt); err != nil { - return diag.FromErr(err) - } - if err = d.Set("private_vulnerability_reporting", configuration.GetPrivateVulnerabilityReporting()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("enforcement", configuration.GetEnforcement()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("target_type", configuration.GetTargetType()); err != nil { - return diag.FromErr(err) + if diags := setCodeSecurityConfigurationState(d, configuration); diags != nil { + return diags } tflog.Trace(ctx, fmt.Sprintf("Successfully read organization code security configuration: %s/%d", org, id), map[string]any{ @@ -490,7 +410,7 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, "id": id, }) - config := expandCodeSecurityConfiguration(d) + config := expandCodeSecurityConfigurationCommon(d) _, _, err = client.Organizations.UpdateCodeSecurityConfiguration(ctx, org, id, config) if err != nil { @@ -558,6 +478,3 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, return nil } -func expandCodeSecurityConfiguration(d *schema.ResourceData) github.CodeSecurityConfiguration { - return expandCodeSecurityConfigurationCommon(d) -} diff --git a/github/util_security_configuration.go b/github/util_security_configuration.go index 3bdb486bb1..ea3d5fdafb 100644 --- a/github/util_security_configuration.go +++ b/github/util_security_configuration.go @@ -2,6 +2,7 @@ package github import ( "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -63,6 +64,95 @@ func flattenSecretScanningDelegatedBypassOptions(options *github.SecretScanningD return []any{bypassOpts} } +// setCodeSecurityConfigurationState writes all shared CodeSecurityConfiguration fields to Terraform state. +// Used by both the organization and enterprise security configuration resources. +func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *github.CodeSecurityConfiguration) diag.Diagnostics { + if err := d.Set("name", configuration.Name); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", configuration.Description); err != nil { + return diag.FromErr(err) + } + if err := d.Set("advanced_security", configuration.GetAdvancedSecurity()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependency_graph", configuration.GetDependencyGraph()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependency_graph_autosubmit_action", configuration.GetDependencyGraphAutosubmitAction()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependency_graph_autosubmit_action_options", flattenDependencyGraphAutosubmitActionOptions(configuration.DependencyGraphAutosubmitActionOptions)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependabot_alerts", configuration.GetDependabotAlerts()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("dependabot_security_updates", configuration.GetDependabotSecurityUpdates()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_default_setup", configuration.GetCodeScanningDefaultSetup()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_default_setup_options", flattenCodeScanningDefaultSetupOptions(configuration.CodeScanningDefaultSetupOptions)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_delegated_alert_dismissal", configuration.GetCodeScanningDelegatedAlertDismissal()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("code_scanning_options", flattenCodeScanningOptions(configuration.CodeScanningOptions)); err != nil { + return diag.FromErr(err) + } + codeSec := configuration.GetCodeSecurity() + if codeSec == "" { + codeSec = "disabled" + } + if err := d.Set("code_security", codeSec); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning", configuration.GetSecretScanning()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_push_protection", configuration.GetSecretScanningPushProtection()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_validity_checks", configuration.GetSecretScanningValidityChecks()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_non_provider_patterns", configuration.GetSecretScanningNonProviderPatterns()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_generic_secrets", configuration.GetSecretScanningGenericSecrets()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("secret_scanning_delegated_alert_dismissal", configuration.GetSecretScanningDelegatedAlertDismissal()); err != nil { + return diag.FromErr(err) + } + secretProt := configuration.GetSecretProtection() + if secretProt == "" { + secretProt = "disabled" + } + if err := d.Set("secret_protection", secretProt); err != nil { + return diag.FromErr(err) + } + if err := d.Set("private_vulnerability_reporting", configuration.GetPrivateVulnerabilityReporting()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("enforcement", configuration.GetEnforcement()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("target_type", configuration.GetTargetType()); err != nil { + return diag.FromErr(err) + } + return nil +} + // expandCodeSecurityConfigurationCommon builds a CodeSecurityConfiguration from Terraform resource data. // Used by both the organization and enterprise security configuration resources. func expandCodeSecurityConfigurationCommon(d *schema.ResourceData) github.CodeSecurityConfiguration { From c6016f6fe707ea3d15cc7bc1ed732604636f5cdd Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:07:00 +1100 Subject: [PATCH 03/12] fix: address remaining reviewer feedback from PR #3143 - Remove fmt.Sprintf from all tflog calls; use static messages with structured fields map for dynamic data (28 instances fixed) - Add configuration_id Computed field to both resources so the numeric config ID is stored separately in state - Update/Delete now read enterprise_slug and configuration_id from state via d.Get() instead of parsing the composite ID - Update enterprise docs with configuration_id attribute Co-Authored-By: Claude Opus 4.6 --- ...ithub_enterprise_security_configuration.go | 61 ++++++++----------- ...hub_organization_security_configuration.go | 58 +++++++----------- github/util_security_configuration.go | 3 + ...prise_security_configuration.html.markdown | 1 + 4 files changed, 52 insertions(+), 71 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 079ace0c9f..1370e76d4d 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -32,6 +32,11 @@ func resourceGithubEnterpriseSecurityConfiguration() *schema.Resource { ForceNew: true, Description: "The slug of the enterprise.", }, + "configuration_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The numeric ID of the code security configuration.", + }, "name": { Type: schema.TypeString, Required: true, @@ -303,7 +308,7 @@ func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d enterprise := d.Get("enterprise_slug").(string) name := d.Get("name").(string) - tflog.Debug(ctx, fmt.Sprintf("Creating enterprise code security configuration: %s/%s", enterprise, name), map[string]any{ + tflog.Debug(ctx, "Creating enterprise code security configuration", map[string]any{ "enterprise": enterprise, "name": name, }) @@ -312,7 +317,7 @@ func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d configuration, _, err := client.Enterprise.CreateCodeSecurityConfiguration(ctx, enterprise, config) if err != nil { - tflog.Error(ctx, fmt.Sprintf("Failed to create enterprise code security configuration: %s/%s", enterprise, name), map[string]any{ + tflog.Error(ctx, "Failed to create enterprise code security configuration", map[string]any{ "enterprise": enterprise, "name": name, "error": err.Error(), @@ -326,7 +331,7 @@ func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d } d.SetId(id) - tflog.Info(ctx, fmt.Sprintf("Created enterprise code security configuration: %s/%s (ID: %d)", enterprise, name, configuration.GetID()), map[string]any{ + tflog.Info(ctx, "Created enterprise code security configuration", map[string]any{ "enterprise": enterprise, "name": name, "id": configuration.GetID(), @@ -348,7 +353,7 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s return diag.FromErr(err) } - tflog.Trace(ctx, fmt.Sprintf("Reading enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Trace(ctx, "Reading enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, }) @@ -358,7 +363,7 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Removing enterprise code security configuration %s/%d from state because it no longer exists in GitHub", enterprise, id), map[string]any{ + tflog.Info(ctx, "Removing enterprise code security configuration from state because it no longer exists in GitHub", map[string]any{ "enterprise": enterprise, "id": id, }) @@ -366,7 +371,7 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s return nil } } - tflog.Error(ctx, fmt.Sprintf("Failed to read enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Error(ctx, "Failed to read enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, "error": err.Error(), @@ -382,7 +387,7 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s return diags } - tflog.Trace(ctx, fmt.Sprintf("Successfully read enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Trace(ctx, "Successfully read enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, }) @@ -392,27 +397,19 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client + enterprise := d.Get("enterprise_slug").(string) + id := int64(d.Get("configuration_id").(int)) - enterprise, idStr, err := parseID2(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } - - tflog.Debug(ctx, fmt.Sprintf("Updating enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Debug(ctx, "Updating enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, }) config := expandCodeSecurityConfigurationCommon(d) - _, _, err = client.Enterprise.UpdateCodeSecurityConfiguration(ctx, enterprise, id, config) + _, _, err := client.Enterprise.UpdateCodeSecurityConfiguration(ctx, enterprise, id, config) if err != nil { - tflog.Error(ctx, fmt.Sprintf("Failed to update enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Error(ctx, "Failed to update enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, "error": err.Error(), @@ -420,7 +417,7 @@ func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d return diag.FromErr(err) } - tflog.Info(ctx, fmt.Sprintf("Updated enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Info(ctx, "Updated enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, }) @@ -430,33 +427,25 @@ func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d func resourceGithubEnterpriseSecurityConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client + enterprise := d.Get("enterprise_slug").(string) + id := int64(d.Get("configuration_id").(int)) - enterprise, idStr, err := parseID2(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } - - tflog.Debug(ctx, fmt.Sprintf("Deleting enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Debug(ctx, "Deleting enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, }) - _, err = client.Enterprise.DeleteCodeSecurityConfiguration(ctx, enterprise, id) + _, err := client.Enterprise.DeleteCodeSecurityConfiguration(ctx, enterprise, id) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Enterprise code security configuration %s/%d already deleted", enterprise, id), map[string]any{ + tflog.Info(ctx, "Enterprise code security configuration already deleted", map[string]any{ "enterprise": enterprise, "id": id, }) return nil } - tflog.Error(ctx, fmt.Sprintf("Failed to delete enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Error(ctx, "Failed to delete enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, "error": err.Error(), @@ -464,7 +453,7 @@ func resourceGithubEnterpriseSecurityConfigurationDelete(ctx context.Context, d return diag.FromErr(err) } - tflog.Info(ctx, fmt.Sprintf("Deleted enterprise code security configuration: %s/%d", enterprise, id), map[string]any{ + tflog.Info(ctx, "Deleted enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, }) diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index b407aa6469..16109e78ff 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -3,7 +3,6 @@ package github import ( "context" "errors" - "fmt" "net/http" "strconv" @@ -26,6 +25,11 @@ func resourceGithubOrganizationSecurityConfiguration() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "configuration_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The numeric ID of the code security configuration.", + }, "name": { Type: schema.TypeString, Required: true, @@ -301,7 +305,7 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, org := meta.(*Owner).name name := d.Get("name").(string) - tflog.Debug(ctx, fmt.Sprintf("Creating organization code security configuration: %s/%s", org, name), map[string]any{ + tflog.Debug(ctx, "Creating organization code security configuration", map[string]any{ "organization": org, "name": name, }) @@ -310,7 +314,7 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, configuration, _, err := client.Organizations.CreateCodeSecurityConfiguration(ctx, org, config) if err != nil { - tflog.Error(ctx, fmt.Sprintf("Failed to create organization code security configuration: %s/%s", org, name), map[string]any{ + tflog.Error(ctx, "Failed to create organization code security configuration", map[string]any{ "organization": org, "name": name, "error": err.Error(), @@ -324,7 +328,7 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, } d.SetId(id) - tflog.Info(ctx, fmt.Sprintf("Created organization code security configuration: %s/%s (ID: %d)", org, name, configuration.GetID()), map[string]any{ + tflog.Info(ctx, "Created organization code security configuration", map[string]any{ "organization": org, "name": name, "id": configuration.GetID(), @@ -350,7 +354,7 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d return diag.FromErr(err) } - tflog.Trace(ctx, fmt.Sprintf("Reading organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Trace(ctx, "Reading organization code security configuration", map[string]any{ "organization": org, "id": id, }) @@ -360,7 +364,7 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Removing organization code security configuration %s/%d from state because it no longer exists in GitHub", org, id), map[string]any{ + tflog.Info(ctx, "Removing organization code security configuration from state because it no longer exists in GitHub", map[string]any{ "organization": org, "id": id, }) @@ -368,7 +372,7 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d return nil } } - tflog.Error(ctx, fmt.Sprintf("Failed to read organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Error(ctx, "Failed to read organization code security configuration", map[string]any{ "organization": org, "id": id, "error": err.Error(), @@ -380,7 +384,7 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d return diags } - tflog.Trace(ctx, fmt.Sprintf("Successfully read organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Trace(ctx, "Successfully read organization code security configuration", map[string]any{ "organization": org, "id": id, }) @@ -394,18 +398,10 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, return diag.FromErr(err) } client := meta.(*Owner).v3client + org := meta.(*Owner).name + id := int64(d.Get("configuration_id").(int)) - org, idStr, err := parseID2(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } - - tflog.Debug(ctx, fmt.Sprintf("Updating organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Debug(ctx, "Updating organization code security configuration", map[string]any{ "organization": org, "id": id, }) @@ -414,7 +410,7 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, _, _, err = client.Organizations.UpdateCodeSecurityConfiguration(ctx, org, id, config) if err != nil { - tflog.Error(ctx, fmt.Sprintf("Failed to update organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Error(ctx, "Failed to update organization code security configuration", map[string]any{ "organization": org, "id": id, "error": err.Error(), @@ -422,7 +418,7 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, return diag.FromErr(err) } - tflog.Info(ctx, fmt.Sprintf("Updated organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Info(ctx, "Updated organization code security configuration", map[string]any{ "organization": org, "id": id, }) @@ -436,18 +432,10 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, return diag.FromErr(err) } client := meta.(*Owner).v3client + org := meta.(*Owner).name + id := int64(d.Get("configuration_id").(int)) - org, idStr, err := parseID2(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } - - tflog.Debug(ctx, fmt.Sprintf("Deleting organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Debug(ctx, "Deleting organization code security configuration", map[string]any{ "organization": org, "id": id, }) @@ -456,13 +444,13 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Organization code security configuration %s/%d already deleted", org, id), map[string]any{ + tflog.Info(ctx, "Organization code security configuration already deleted", map[string]any{ "organization": org, "id": id, }) return nil } - tflog.Error(ctx, fmt.Sprintf("Failed to delete organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Error(ctx, "Failed to delete organization code security configuration", map[string]any{ "organization": org, "id": id, "error": err.Error(), @@ -470,7 +458,7 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, return diag.FromErr(err) } - tflog.Info(ctx, fmt.Sprintf("Deleted organization code security configuration: %s/%d", org, id), map[string]any{ + tflog.Info(ctx, "Deleted organization code security configuration", map[string]any{ "organization": org, "id": id, }) diff --git a/github/util_security_configuration.go b/github/util_security_configuration.go index ea3d5fdafb..34d705b4d4 100644 --- a/github/util_security_configuration.go +++ b/github/util_security_configuration.go @@ -67,6 +67,9 @@ func flattenSecretScanningDelegatedBypassOptions(options *github.SecretScanningD // setCodeSecurityConfigurationState writes all shared CodeSecurityConfiguration fields to Terraform state. // Used by both the organization and enterprise security configuration resources. func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *github.CodeSecurityConfiguration) diag.Diagnostics { + if err := d.Set("configuration_id", configuration.GetID()); err != nil { + return diag.FromErr(err) + } if err := d.Set("name", configuration.Name); err != nil { return diag.FromErr(err) } diff --git a/website/docs/r/enterprise_security_configuration.html.markdown b/website/docs/r/enterprise_security_configuration.html.markdown index 8aaa4edfdc..728575382e 100644 --- a/website/docs/r/enterprise_security_configuration.html.markdown +++ b/website/docs/r/enterprise_security_configuration.html.markdown @@ -60,6 +60,7 @@ The following arguments are supported: ## Attributes Reference +* `configuration_id` - The numeric ID of the code security configuration. * `target_type` - The target type of the code security configuration. ### Dependency Graph Autosubmit Action Options From 20f4c1775241801e4a5b155160810e8792c46844 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:12:28 +1100 Subject: [PATCH 04/12] docs: add organization security configuration docs, test configuration_id - Add missing organization_security_configuration documentation - Fix enterprise docs: description is Optional not Required - Add configuration_id assertions to both test files Co-Authored-By: Claude Opus 4.6 --- ..._enterprise_security_configuration_test.go | 2 + ...rganization_security_configuration_test.go | 2 + ...prise_security_configuration.html.markdown | 2 +- ...ation_security_configuration.html.markdown | 97 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 website/docs/r/organization_security_configuration.html.markdown diff --git a/github/resource_github_enterprise_security_configuration_test.go b/github/resource_github_enterprise_security_configuration_test.go index 5f45863a5f..c3944068be 100644 --- a/github/resource_github_enterprise_security_configuration_test.go +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -47,6 +47,8 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { tfjsonpath.New("advanced_security"), knownvalue.StringExact("enabled")), statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", tfjsonpath.New("enforcement"), knownvalue.StringExact("enforced")), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + tfjsonpath.New("configuration_id"), knownvalue.NotNull()), }, }, { diff --git a/github/resource_github_organization_security_configuration_test.go b/github/resource_github_organization_security_configuration_test.go index 65ced074b8..16efe4cc5a 100644 --- a/github/resource_github_organization_security_configuration_test.go +++ b/github/resource_github_organization_security_configuration_test.go @@ -46,6 +46,8 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { tfjsonpath.New("advanced_security"), knownvalue.StringExact("enabled")), statecheck.ExpectKnownValue("github_organization_security_configuration.test", tfjsonpath.New("enforcement"), knownvalue.StringExact("enforced")), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + tfjsonpath.New("configuration_id"), knownvalue.NotNull()), }, }, { diff --git a/website/docs/r/enterprise_security_configuration.html.markdown b/website/docs/r/enterprise_security_configuration.html.markdown index 728575382e..2bb5404638 100644 --- a/website/docs/r/enterprise_security_configuration.html.markdown +++ b/website/docs/r/enterprise_security_configuration.html.markdown @@ -34,7 +34,7 @@ The following arguments are supported: * `enterprise_slug` - (Required) The slug of the enterprise. Changing this forces a new resource to be created. * `name` - (Required) The name of the code security configuration. -* `description` - (Required) A description of the code security configuration. +* `description` - (Optional) A description of the code security configuration. * `advanced_security` - (Optional) The advanced security configuration. Can be one of `enabled`, `disabled`. * `dependency_graph` - (Optional) The dependency graph configuration. Can be one of `enabled`, `disabled`, `not_set`. * `dependency_graph_autosubmit_action` - (Optional) The dependency graph autosubmit action configuration. Can be one of `enabled`, `disabled`, `not_set`. diff --git a/website/docs/r/organization_security_configuration.html.markdown b/website/docs/r/organization_security_configuration.html.markdown new file mode 100644 index 0000000000..dbbf7a522d --- /dev/null +++ b/website/docs/r/organization_security_configuration.html.markdown @@ -0,0 +1,97 @@ +--- +layout: "github" +page_title: "GitHub: github_organization_security_configuration" +description: |- + Manages a code security configuration for a GitHub Organization. +--- + +# github_organization_security_configuration + +This resource allows you to create and manage code security configurations for a GitHub Organization. + +## Example Usage + +```hcl +resource "github_organization_security_configuration" "default" { + name = "default-config" + description = "Default security configuration" + advanced_security = "enabled" + dependency_graph = "enabled" + dependabot_alerts = "enabled" + dependabot_security_updates = "enabled" + code_scanning_default_setup = "enabled" + secret_scanning = "enabled" + secret_scanning_push_protection = "enabled" + private_vulnerability_reporting = "enabled" + enforcement = "enforced" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the code security configuration. +* `description` - (Optional) A description of the code security configuration. +* `advanced_security` - (Optional) The advanced security configuration. Can be one of `enabled`, `disabled`. +* `dependency_graph` - (Optional) The dependency graph configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `dependency_graph_autosubmit_action` - (Optional) The dependency graph autosubmit action configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `dependency_graph_autosubmit_action_options` - (Optional) The dependency graph autosubmit action options. See [Dependency Graph Autosubmit Action Options](#dependency-graph-autosubmit-action-options) below for details. +* `dependabot_alerts` - (Optional) The dependabot alerts configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `dependabot_security_updates` - (Optional) The dependabot security updates configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `code_scanning_default_setup` - (Optional) The code scanning default setup configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `code_scanning_default_setup_options` - (Optional) The code scanning default setup options. See [Code Scanning Default Setup Options](#code-scanning-default-setup-options) below for details. +* `code_scanning_delegated_alert_dismissal` - (Optional) The code scanning delegated alert dismissal configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `code_scanning_options` - (Optional) The code scanning options. See [Code Scanning Options](#code-scanning-options) below for details. +* `code_security` - (Optional) The code security configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning` - (Optional) The secret scanning configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_push_protection` - (Optional) The secret scanning push protection configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_delegated_bypass` - (Optional) The secret scanning delegated bypass configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_delegated_bypass_options` - (Optional) The secret scanning delegated bypass options. See [Secret Scanning Delegated Bypass Options](#secret-scanning-delegated-bypass-options) below for details. +* `secret_scanning_validity_checks` - (Optional) The secret scanning validity checks configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_non_provider_patterns` - (Optional) The secret scanning non provider patterns configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_generic_secrets` - (Optional) The secret scanning generic secrets configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_scanning_delegated_alert_dismissal` - (Optional) The secret scanning delegated alert dismissal configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `secret_protection` - (Optional) The secret protection configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `private_vulnerability_reporting` - (Optional) The private vulnerability reporting configuration. Can be one of `enabled`, `disabled`, `not_set`. +* `enforcement` - (Optional) The enforcement configuration. Can be one of `enforced`, `unenforced`. + +## Attributes Reference + +* `configuration_id` - The numeric ID of the code security configuration. +* `target_type` - The target type of the code security configuration. + +### Dependency Graph Autosubmit Action Options + +The `dependency_graph_autosubmit_action_options` block supports: + +* `labeled_runners` - (Optional) Whether to use labeled runners for the dependency graph autosubmit action. + +### Code Scanning Default Setup Options + +The `code_scanning_default_setup_options` block supports: + +* `runner_type` - (Optional) The type of runner to use for code scanning default setup. Can be one of `standard`, `labeled`. +* `runner_label` - (Optional) The label of the runner to use for code scanning default setup. + +### Code Scanning Options + +The `code_scanning_options` block supports: + +* `allow_advanced` - (Optional) Whether to allow advanced security for code scanning. + +### Secret Scanning Delegated Bypass Options + +The `secret_scanning_delegated_bypass_options` block supports: + +* `reviewers` - (Optional) The bypass reviewers. Each entry supports: + * `reviewer_id` - (Required) The ID of the bypass reviewer (team or role ID). + * `reviewer_type` - (Required) The type of the bypass reviewer. Can be one of `Team`, `Role`. + +## Import + +GitHub Organization Code Security Configurations can be imported using the organization name and the configuration ID separated by a colon, e.g. + +```text +$ terraform import github_organization_security_configuration.example my-org:123 +``` From 8df8d10b2768be06829f2d1a5f5866380442736a Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:06:56 +1100 Subject: [PATCH 05/12] fix: upgrade go-github v84, address remaining PR #3143 review feedback - Upgrade go-github imports from v83 to v84 across all feature files - Remove secret_scanning_delegated_bypass from enterprise resource (org-only API) - Fix reviewer_type enum casing to TEAM/ROLE to match GitHub API - Wire expandSecretScanningDelegatedBypass into org Create/Update - Remove hardcoded "disabled" defaults for code_security/secret_protection - Use GetOk for description field in expand (consistency with other Optional fields) - Add unit tests for all flatten utility functions (deiga requested) - Add missing ImportState steps to acceptance tests Co-Authored-By: Claude Opus 4.6 --- ...ithub_enterprise_security_configuration.go | 41 +---- ..._enterprise_security_configuration_test.go | 40 +---- ...hub_organization_security_configuration.go | 14 +- ...rganization_security_configuration_test.go | 9 +- github/util_security_configuration.go | 40 ++--- github/util_security_configuration_test.go | 142 +++++++++++++++++- ...prise_security_configuration.html.markdown | 10 -- ...ation_security_configuration.html.markdown | 2 +- 8 files changed, 181 insertions(+), 117 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 1370e76d4d..3e336e9d22 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -7,7 +7,7 @@ import ( "net/http" "strconv" - "github.com/google/go-github/v83/github" + "github.com/google/go-github/v84/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -192,45 +192,6 @@ func resourceGithubEnterpriseSecurityConfiguration() *schema.Resource { "enabled", "disabled", "not_set", }, false)), }, - "secret_scanning_delegated_bypass": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "The secret scanning delegated bypass configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ - "enabled", "disabled", "not_set", - }, false)), - }, - "secret_scanning_delegated_bypass_options": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "The secret scanning delegated bypass options for the code security configuration.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "reviewers": { - Type: schema.TypeList, - Optional: true, - Description: "The bypass reviewers for the secret scanning delegated bypass.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "reviewer_id": { - Type: schema.TypeInt, - Required: true, - Description: "The ID of the bypass reviewer.", - }, - "reviewer_type": { - Type: schema.TypeString, - Required: true, - Description: "The type of the bypass reviewer. Can be one of 'Team', 'Role'.", - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"Team", "Role"}, false)), - }, - }, - }, - }, - }, - }, - }, "secret_scanning_validity_checks": { Type: schema.TypeString, Optional: true, diff --git a/github/resource_github_enterprise_security_configuration_test.go b/github/resource_github_enterprise_security_configuration_test.go index c3944068be..04e242da91 100644 --- a/github/resource_github_enterprise_security_configuration_test.go +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -174,45 +174,13 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { tfjsonpath.New("target_type"), knownvalue.NotNull()), }, }, - }, - }) - }) - - t.Run("creates enterprise security configuration with delegated bypass options", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-bypass-%s", randomID) - - config := fmt.Sprintf(` - resource "github_enterprise_security_configuration" "test" { - enterprise_slug = "%s" - name = "%s" - description = "Test configuration with delegated bypass" - advanced_security = "enabled" - secret_scanning = "enabled" - secret_scanning_push_protection = "enabled" - secret_scanning_delegated_bypass = "enabled" - secret_scanning_delegated_bypass_options { - reviewers { - reviewer_id = 1 - reviewer_type = "Team" - } - } - }`, testAccConf.enterpriseSlug, configName) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessEnterprise(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ { - Config: config, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", - tfjsonpath.New("secret_scanning_delegated_bypass"), knownvalue.StringExact("enabled")), - statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", - tfjsonpath.New("secret_scanning_delegated_bypass_options").AtSliceIndex(0).AtMapKey("reviewers").AtSliceIndex(0).AtMapKey("reviewer_type"), knownvalue.StringExact("Team")), - }, + ResourceName: "github_enterprise_security_configuration.test", + ImportState: true, + ImportStateVerify: true, }, }, }) }) + } diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index 16109e78ff..49a5788425 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -6,7 +6,7 @@ import ( "net/http" "strconv" - "github.com/google/go-github/v83/github" + "github.com/google/go-github/v84/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -215,8 +215,8 @@ func resourceGithubOrganizationSecurityConfiguration() *schema.Resource { "reviewer_type": { Type: schema.TypeString, Required: true, - Description: "The type of the bypass reviewer. Can be one of 'Team', 'Role'.", - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"Team", "Role"}, false)), + Description: "The type of the bypass reviewer. Can be one of 'TEAM', 'ROLE'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"TEAM", "ROLE"}, false)), }, }, }, @@ -311,6 +311,7 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, }) config := expandCodeSecurityConfigurationCommon(d) + expandSecretScanningDelegatedBypass(d, &config) configuration, _, err := client.Organizations.CreateCodeSecurityConfiguration(ctx, org, config) if err != nil { @@ -383,6 +384,12 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d if diags := setCodeSecurityConfigurationState(d, configuration); diags != nil { return diags } + if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { + return diag.FromErr(err) + } tflog.Trace(ctx, "Successfully read organization code security configuration", map[string]any{ "organization": org, @@ -407,6 +414,7 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, }) config := expandCodeSecurityConfigurationCommon(d) + expandSecretScanningDelegatedBypass(d, &config) _, _, err = client.Organizations.UpdateCodeSecurityConfiguration(ctx, org, id, config) if err != nil { diff --git a/github/resource_github_organization_security_configuration_test.go b/github/resource_github_organization_security_configuration_test.go index 16efe4cc5a..0ea2acab3e 100644 --- a/github/resource_github_organization_security_configuration_test.go +++ b/github/resource_github_organization_security_configuration_test.go @@ -195,7 +195,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { secret_scanning_delegated_bypass_options { reviewers { reviewer_id = 1 - reviewer_type = "Team" + reviewer_type = "TEAM" } } }`, configName) @@ -210,9 +210,14 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { statecheck.ExpectKnownValue("github_organization_security_configuration.test", tfjsonpath.New("secret_scanning_delegated_bypass"), knownvalue.StringExact("enabled")), statecheck.ExpectKnownValue("github_organization_security_configuration.test", - tfjsonpath.New("secret_scanning_delegated_bypass_options").AtSliceIndex(0).AtMapKey("reviewers").AtSliceIndex(0).AtMapKey("reviewer_type"), knownvalue.StringExact("Team")), + tfjsonpath.New("secret_scanning_delegated_bypass_options").AtSliceIndex(0).AtMapKey("reviewers").AtSliceIndex(0).AtMapKey("reviewer_type"), knownvalue.StringExact("TEAM")), }, }, + { + ResourceName: "github_organization_security_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) }) diff --git a/github/util_security_configuration.go b/github/util_security_configuration.go index 34d705b4d4..42187f5726 100644 --- a/github/util_security_configuration.go +++ b/github/util_security_configuration.go @@ -1,7 +1,7 @@ package github import ( - "github.com/google/go-github/v83/github" + "github.com/google/go-github/v84/github" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -106,11 +106,7 @@ func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *gi if err := d.Set("code_scanning_options", flattenCodeScanningOptions(configuration.CodeScanningOptions)); err != nil { return diag.FromErr(err) } - codeSec := configuration.GetCodeSecurity() - if codeSec == "" { - codeSec = "disabled" - } - if err := d.Set("code_security", codeSec); err != nil { + if err := d.Set("code_security", configuration.GetCodeSecurity()); err != nil { return diag.FromErr(err) } if err := d.Set("secret_scanning", configuration.GetSecretScanning()); err != nil { @@ -119,12 +115,6 @@ func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *gi if err := d.Set("secret_scanning_push_protection", configuration.GetSecretScanningPushProtection()); err != nil { return diag.FromErr(err) } - if err := d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { - return diag.FromErr(err) - } if err := d.Set("secret_scanning_validity_checks", configuration.GetSecretScanningValidityChecks()); err != nil { return diag.FromErr(err) } @@ -137,11 +127,7 @@ func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *gi if err := d.Set("secret_scanning_delegated_alert_dismissal", configuration.GetSecretScanningDelegatedAlertDismissal()); err != nil { return diag.FromErr(err) } - secretProt := configuration.GetSecretProtection() - if secretProt == "" { - secretProt = "disabled" - } - if err := d.Set("secret_protection", secretProt); err != nil { + if err := d.Set("secret_protection", configuration.GetSecretProtection()); err != nil { return diag.FromErr(err) } if err := d.Set("private_vulnerability_reporting", configuration.GetPrivateVulnerabilityReporting()); err != nil { @@ -160,8 +146,10 @@ func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *gi // Used by both the organization and enterprise security configuration resources. func expandCodeSecurityConfigurationCommon(d *schema.ResourceData) github.CodeSecurityConfiguration { config := github.CodeSecurityConfiguration{ - Name: d.Get("name").(string), - Description: d.Get("description").(string), + Name: d.Get("name").(string), + } + if val, ok := d.GetOk("description"); ok { + config.Description = val.(string) } if val, ok := d.GetOk("advanced_security"); ok { @@ -194,9 +182,6 @@ func expandCodeSecurityConfigurationCommon(d *schema.ResourceData) github.CodeSe if val, ok := d.GetOk("secret_scanning_push_protection"); ok { config.SecretScanningPushProtection = github.Ptr(val.(string)) } - if val, ok := d.GetOk("secret_scanning_delegated_bypass"); ok { - config.SecretScanningDelegatedBypass = github.Ptr(val.(string)) - } if val, ok := d.GetOk("secret_scanning_validity_checks"); ok { config.SecretScanningValidityChecks = github.Ptr(val.(string)) } @@ -252,6 +237,15 @@ func expandCodeSecurityConfigurationCommon(d *schema.ResourceData) github.CodeSe } } + return config +} + +// expandSecretScanningDelegatedBypass adds secret_scanning_delegated_bypass fields to a CodeSecurityConfiguration. +// These fields are only supported by the organization API, not the enterprise API. +func expandSecretScanningDelegatedBypass(d *schema.ResourceData, config *github.CodeSecurityConfiguration) { + if val, ok := d.GetOk("secret_scanning_delegated_bypass"); ok { + config.SecretScanningDelegatedBypass = github.Ptr(val.(string)) + } if val, ok := d.GetOk("secret_scanning_delegated_bypass_options"); ok { optionsList := val.([]any) if len(optionsList) > 0 { @@ -272,6 +266,4 @@ func expandCodeSecurityConfigurationCommon(d *schema.ResourceData) github.CodeSe config.SecretScanningDelegatedBypassOptions = options } } - - return config } diff --git a/github/util_security_configuration_test.go b/github/util_security_configuration_test.go index db1a703d16..1f996bfd5b 100644 --- a/github/util_security_configuration_test.go +++ b/github/util_security_configuration_test.go @@ -3,9 +3,149 @@ package github import ( "testing" - "github.com/google/go-github/v83/github" + "github.com/google/go-github/v84/github" ) +func TestFlattenDependencyGraphAutosubmitActionOptions(t *testing.T) { + t.Run("returns empty slice when options is nil", func(t *testing.T) { + result := flattenDependencyGraphAutosubmitActionOptions(nil) + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }) + + t.Run("omits labeled_runners key when LabeledRunners is nil", func(t *testing.T) { + opts := &github.DependencyGraphAutosubmitActionOptions{} + result := flattenDependencyGraphAutosubmitActionOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["labeled_runners"]; ok { + t.Errorf("labeled_runners should be absent when LabeledRunners is nil") + } + }) + + t.Run("sets labeled_runners when LabeledRunners is non-nil", func(t *testing.T) { + opts := &github.DependencyGraphAutosubmitActionOptions{ + LabeledRunners: github.Ptr(true), + } + result := flattenDependencyGraphAutosubmitActionOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["labeled_runners"] != true { + t.Errorf("expected labeled_runners true, got %v", m["labeled_runners"]) + } + }) +} + +func TestFlattenCodeScanningOptions(t *testing.T) { + t.Run("returns empty slice when options is nil", func(t *testing.T) { + result := flattenCodeScanningOptions(nil) + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }) + + t.Run("omits allow_advanced key when AllowAdvanced is nil", func(t *testing.T) { + opts := &github.CodeScanningOptions{} + result := flattenCodeScanningOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["allow_advanced"]; ok { + t.Errorf("allow_advanced should be absent when AllowAdvanced is nil") + } + }) + + t.Run("sets allow_advanced when AllowAdvanced is true", func(t *testing.T) { + opts := &github.CodeScanningOptions{ + AllowAdvanced: github.Ptr(true), + } + result := flattenCodeScanningOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["allow_advanced"] != true { + t.Errorf("expected allow_advanced true, got %v", m["allow_advanced"]) + } + }) + + t.Run("sets allow_advanced when AllowAdvanced is false", func(t *testing.T) { + opts := &github.CodeScanningOptions{ + AllowAdvanced: github.Ptr(false), + } + result := flattenCodeScanningOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["allow_advanced"] != false { + t.Errorf("expected allow_advanced false, got %v", m["allow_advanced"]) + } + }) +} + +func TestFlattenSecretScanningDelegatedBypassOptions(t *testing.T) { + t.Run("returns empty slice when options is nil", func(t *testing.T) { + result := flattenSecretScanningDelegatedBypassOptions(nil) + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }) + + t.Run("omits reviewers key when Reviewers is nil", func(t *testing.T) { + opts := &github.SecretScanningDelegatedBypassOptions{} + result := flattenSecretScanningDelegatedBypassOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["reviewers"]; ok { + t.Errorf("reviewers should be absent when Reviewers is nil") + } + }) + + t.Run("sets reviewers when Reviewers is populated", func(t *testing.T) { + opts := &github.SecretScanningDelegatedBypassOptions{ + Reviewers: []*github.BypassReviewer{ + {ReviewerID: 42, ReviewerType: "TEAM"}, + {ReviewerID: 99, ReviewerType: "ROLE"}, + }, + } + result := flattenSecretScanningDelegatedBypassOptions(opts) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + reviewers, ok := m["reviewers"].([]any) + if !ok { + t.Fatalf("expected reviewers to be []any, got %T", m["reviewers"]) + } + if len(reviewers) != 2 { + t.Fatalf("expected 2 reviewers, got %d", len(reviewers)) + } + first := reviewers[0].(map[string]any) + if first["reviewer_id"] != int64(42) { + t.Errorf("expected reviewer_id 42, got %v", first["reviewer_id"]) + } + if first["reviewer_type"] != "TEAM" { + t.Errorf("expected reviewer_type TEAM, got %v", first["reviewer_type"]) + } + second := reviewers[1].(map[string]any) + if second["reviewer_id"] != int64(99) { + t.Errorf("expected reviewer_id 99, got %v", second["reviewer_id"]) + } + if second["reviewer_type"] != "ROLE" { + t.Errorf("expected reviewer_type ROLE, got %v", second["reviewer_type"]) + } + }) +} + func TestFlattenCodeScanningDefaultSetupOptions(t *testing.T) { t.Run("returns empty slice when options is nil", func(t *testing.T) { result := flattenCodeScanningDefaultSetupOptions(nil) diff --git a/website/docs/r/enterprise_security_configuration.html.markdown b/website/docs/r/enterprise_security_configuration.html.markdown index 2bb5404638..8df70c34a4 100644 --- a/website/docs/r/enterprise_security_configuration.html.markdown +++ b/website/docs/r/enterprise_security_configuration.html.markdown @@ -48,8 +48,6 @@ The following arguments are supported: * `code_security` - (Optional) The code security configuration. Can be one of `enabled`, `disabled`, `not_set`. * `secret_scanning` - (Optional) The secret scanning configuration. Can be one of `enabled`, `disabled`, `not_set`. * `secret_scanning_push_protection` - (Optional) The secret scanning push protection configuration. Can be one of `enabled`, `disabled`, `not_set`. -* `secret_scanning_delegated_bypass` - (Optional) The secret scanning delegated bypass configuration. Can be one of `enabled`, `disabled`, `not_set`. -* `secret_scanning_delegated_bypass_options` - (Optional) The secret scanning delegated bypass options. See [Secret Scanning Delegated Bypass Options](#secret-scanning-delegated-bypass-options) below for details. * `secret_scanning_validity_checks` - (Optional) The secret scanning validity checks configuration. Can be one of `enabled`, `disabled`, `not_set`. * `secret_scanning_non_provider_patterns` - (Optional) The secret scanning non provider patterns configuration. Can be one of `enabled`, `disabled`, `not_set`. * `secret_scanning_generic_secrets` - (Optional) The secret scanning generic secrets configuration. Can be one of `enabled`, `disabled`, `not_set`. @@ -82,14 +80,6 @@ The `code_scanning_options` block supports: * `allow_advanced` - (Optional) Whether to allow advanced security for code scanning. -### Secret Scanning Delegated Bypass Options - -The `secret_scanning_delegated_bypass_options` block supports: - -* `reviewers` - (Optional) The bypass reviewers. Each entry supports: - * `reviewer_id` - (Required) The ID of the bypass reviewer (team or role ID). - * `reviewer_type` - (Required) The type of the bypass reviewer. Can be one of `Team`, `Role`. - ## Import GitHub Enterprise Code Security Configurations can be imported using the enterprise slug and the configuration ID separated by a colon, e.g. diff --git a/website/docs/r/organization_security_configuration.html.markdown b/website/docs/r/organization_security_configuration.html.markdown index dbbf7a522d..9236eb3481 100644 --- a/website/docs/r/organization_security_configuration.html.markdown +++ b/website/docs/r/organization_security_configuration.html.markdown @@ -86,7 +86,7 @@ The `secret_scanning_delegated_bypass_options` block supports: * `reviewers` - (Optional) The bypass reviewers. Each entry supports: * `reviewer_id` - (Required) The ID of the bypass reviewer (team or role ID). - * `reviewer_type` - (Required) The type of the bypass reviewer. Can be one of `Team`, `Role`. + * `reviewer_type` - (Required) The type of the bypass reviewer. Can be one of `TEAM`, `ROLE`. ## Import From f6b7b98cb54957dcc4e724ed442c81ceb9d89aeb Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:34:17 +1100 Subject: [PATCH 06/12] fix: address PR #3284 review feedback, align with repo conventions - Add custom importer functions so Read doesn't parse from ID - Read fetches org/enterprise and configuration_id from state - Create/Update return nil instead of calling Read directly - Use diags.HasError() instead of diags != nil - Use testResourcePrefix in all test resource names - Extract import tests into separate t.Run blocks - Inline test HCL templates instead of shared tmpl variables --- ...ithub_enterprise_security_configuration.go | 41 ++++++------ ..._enterprise_security_configuration_test.go | 61 ++++++++++------- ...hub_organization_security_configuration.go | 49 ++++++++++---- ...rganization_security_configuration_test.go | 65 +++++++++++-------- 4 files changed, 133 insertions(+), 83 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 3e336e9d22..51fbe731b7 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -292,27 +292,23 @@ func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d } d.SetId(id) + if err = d.Set("configuration_id", int(configuration.GetID())); err != nil { + return diag.FromErr(err) + } + tflog.Info(ctx, "Created enterprise code security configuration", map[string]any{ "enterprise": enterprise, "name": name, "id": configuration.GetID(), }) - return resourceGithubEnterpriseSecurityConfigurationRead(ctx, d, meta) + return nil } func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client - - enterprise, idStr, err := parseID2(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } + enterprise := d.Get("enterprise_slug").(string) + id := int64(d.Get("configuration_id").(int)) tflog.Trace(ctx, "Reading enterprise code security configuration", map[string]any{ "enterprise": enterprise, @@ -340,11 +336,7 @@ func resourceGithubEnterpriseSecurityConfigurationRead(ctx context.Context, d *s return diag.FromErr(err) } - if err = d.Set("enterprise_slug", enterprise); err != nil { - return diag.FromErr(err) - } - - if diags := setCodeSecurityConfigurationState(d, configuration); diags != nil { + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { return diags } @@ -383,7 +375,7 @@ func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d "id": id, }) - return resourceGithubEnterpriseSecurityConfigurationRead(ctx, d, meta) + return nil } func resourceGithubEnterpriseSecurityConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -422,13 +414,18 @@ func resourceGithubEnterpriseSecurityConfigurationDelete(ctx context.Context, d return nil } -func resourceGithubEnterpriseSecurityConfigurationImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - enterpriseSlug, configID, err := parseID2(d.Id()) +func resourceGithubEnterpriseSecurityConfigurationImport(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + enterpriseSlug, configIDStr, err := parseID2(d.Id()) if err != nil { return nil, fmt.Errorf("invalid import specified: supplied import must be written as :. Parse error: %w", err) } - id, err := buildID(enterpriseSlug, configID) + configID, err := strconv.ParseInt(configIDStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid configuration_id %q: %w", configIDStr, err) + } + + id, err := buildID(enterpriseSlug, configIDStr) if err != nil { return nil, err } @@ -438,6 +435,10 @@ func resourceGithubEnterpriseSecurityConfigurationImport(ctx context.Context, d return nil, err } + if err = d.Set("configuration_id", int(configID)); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_enterprise_security_configuration_test.go b/github/resource_github_enterprise_security_configuration_test.go index 04e242da91..24023a7e16 100644 --- a/github/resource_github_enterprise_security_configuration_test.go +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -14,7 +14,7 @@ import ( func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { t.Run("creates enterprise security configuration without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_enterprise_security_configuration" "test" { @@ -51,6 +51,28 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { tfjsonpath.New("configuration_id"), knownvalue.NotNull()), }, }, + }, + }) + }) + + t.Run("imports enterprise security configuration without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "Test configuration for import" + }`, testAccConf.enterpriseSlug, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + }, { ResourceName: "github_enterprise_security_configuration.test", ImportState: true, @@ -62,18 +84,24 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { t.Run("updates enterprise security configuration without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-%s", randomID) - configNameUpdated := fmt.Sprintf("test-config-updated-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + configNameUpdated := fmt.Sprintf("%supdated-%s", testResourcePrefix, randomID) - tmpl := ` + configBefore := fmt.Sprintf(` resource "github_enterprise_security_configuration" "test" { enterprise_slug = "%s" name = "%s" - description = "%s" - advanced_security = "%s" - }` - configBefore := fmt.Sprintf(tmpl, testAccConf.enterpriseSlug, configName, "Test configuration", "disabled") - configAfter := fmt.Sprintf(tmpl, testAccConf.enterpriseSlug, configNameUpdated, "Test configuration updated", "enabled") + description = "Test configuration" + advanced_security = "disabled" + }`, testAccConf.enterpriseSlug, configName) + + configAfter := fmt.Sprintf(` + resource "github_enterprise_security_configuration" "test" { + enterprise_slug = "%s" + name = "%s" + description = "Test configuration updated" + advanced_security = "enabled" + }`, testAccConf.enterpriseSlug, configNameUpdated) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, @@ -103,7 +131,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { t.Run("creates enterprise security configuration with options", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-options-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_enterprise_security_configuration" "test" { @@ -141,18 +169,13 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { tfjsonpath.New("code_scanning_default_setup_options").AtSliceIndex(0).AtMapKey("runner_label"), knownvalue.StringExact("code-scanning")), }, }, - { - ResourceName: "github_enterprise_security_configuration.test", - ImportState: true, - ImportStateVerify: true, - }, }, }) }) t.Run("creates enterprise security configuration with minimal config", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-minimal-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_enterprise_security_configuration" "test" { @@ -174,13 +197,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { tfjsonpath.New("target_type"), knownvalue.NotNull()), }, }, - { - ResourceName: "github_enterprise_security_configuration.test", - ImportState: true, - ImportStateVerify: true, - }, }, }) }) - } diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index 49a5788425..3b24942eaa 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -3,6 +3,7 @@ package github import ( "context" "errors" + "fmt" "net/http" "strconv" @@ -21,7 +22,7 @@ func resourceGithubOrganizationSecurityConfiguration() *schema.Resource { UpdateContext: resourceGithubOrganizationSecurityConfigurationUpdate, DeleteContext: resourceGithubOrganizationSecurityConfigurationDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubOrganizationSecurityConfigurationImport, }, Schema: map[string]*schema.Schema{ @@ -329,13 +330,17 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, } d.SetId(id) + if err = d.Set("configuration_id", int(configuration.GetID())); err != nil { + return diag.FromErr(err) + } + tflog.Info(ctx, "Created organization code security configuration", map[string]any{ "organization": org, "name": name, "id": configuration.GetID(), }) - return resourceGithubOrganizationSecurityConfigurationRead(ctx, d, meta) + return nil } func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -344,16 +349,8 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d return diag.FromErr(err) } client := meta.(*Owner).v3client - - org, idStr, err := parseID2(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } + org := meta.(*Owner).name + id := int64(d.Get("configuration_id").(int)) tflog.Trace(ctx, "Reading organization code security configuration", map[string]any{ "organization": org, @@ -381,7 +378,7 @@ func resourceGithubOrganizationSecurityConfigurationRead(ctx context.Context, d return diag.FromErr(err) } - if diags := setCodeSecurityConfigurationState(d, configuration); diags != nil { + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { return diags } if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { @@ -431,7 +428,7 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, "id": id, }) - return resourceGithubOrganizationSecurityConfigurationRead(ctx, d, meta) + return nil } func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -474,3 +471,27 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, return nil } +func resourceGithubOrganizationSecurityConfigurationImport(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + orgName, configIDStr, err := parseID2(d.Id()) + if err != nil { + return nil, fmt.Errorf("invalid import specified: supplied import must be written as :. Parse error: %w", err) + } + + configID, err := strconv.ParseInt(configIDStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid configuration_id %q: %w", configIDStr, err) + } + + id, err := buildID(orgName, configIDStr) + if err != nil { + return nil, err + } + d.SetId(id) + + if err = d.Set("configuration_id", int(configID)); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + diff --git a/github/resource_github_organization_security_configuration_test.go b/github/resource_github_organization_security_configuration_test.go index 0ea2acab3e..11fd8c50cd 100644 --- a/github/resource_github_organization_security_configuration_test.go +++ b/github/resource_github_organization_security_configuration_test.go @@ -14,7 +14,7 @@ import ( func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { t.Run("creates organization security configuration without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_security_configuration" "test" { @@ -50,6 +50,27 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { tfjsonpath.New("configuration_id"), knownvalue.NotNull()), }, }, + }, + }) + }) + + t.Run("imports organization security configuration without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "Test configuration for import" + }`, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + }, { ResourceName: "github_organization_security_configuration.test", ImportState: true, @@ -61,17 +82,22 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { t.Run("updates organization security configuration without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-%s", randomID) - configNameUpdated := fmt.Sprintf("test-config-updated-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + configNameUpdated := fmt.Sprintf("%supdated-%s", testResourcePrefix, randomID) - tmpl := ` + configBefore := fmt.Sprintf(` resource "github_organization_security_configuration" "test" { name = "%s" - description = "%s" - advanced_security = "%s" - }` - configBefore := fmt.Sprintf(tmpl, configName, "Test configuration", "disabled") - configAfter := fmt.Sprintf(tmpl, configNameUpdated, "Test configuration updated", "enabled") + description = "Test configuration" + advanced_security = "disabled" + }`, configName) + + configAfter := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + description = "Test configuration updated" + advanced_security = "enabled" + }`, configNameUpdated) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -101,7 +127,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { t.Run("creates organization security configuration with options", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-options-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_security_configuration" "test" { @@ -139,18 +165,13 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { tfjsonpath.New("code_scanning_default_setup_options").AtSliceIndex(0).AtMapKey("runner_label"), knownvalue.StringExact("code-scanning")), }, }, - { - ResourceName: "github_organization_security_configuration.test", - ImportState: true, - ImportStateVerify: true, - }, }, }) }) t.Run("creates organization security configuration with minimal config", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-minimal-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_security_configuration" "test" { @@ -171,18 +192,13 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { tfjsonpath.New("target_type"), knownvalue.NotNull()), }, }, - { - ResourceName: "github_organization_security_configuration.test", - ImportState: true, - ImportStateVerify: true, - }, }, }) }) t.Run("creates organization security configuration with delegated bypass options", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - configName := fmt.Sprintf("test-config-bypass-%s", randomID) + configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_security_configuration" "test" { @@ -213,11 +229,6 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { tfjsonpath.New("secret_scanning_delegated_bypass_options").AtSliceIndex(0).AtMapKey("reviewers").AtSliceIndex(0).AtMapKey("reviewer_type"), knownvalue.StringExact("TEAM")), }, }, - { - ResourceName: "github_organization_security_configuration.test", - ImportState: true, - ImportStateVerify: true, - }, }, }) }) From 83f7972b087b014f9b70e82412bd9b3852f307e0 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:09:56 +1100 Subject: [PATCH 07/12] fix: populate all computed fields in Create/Update instead of calling Read Create and Update functions now set state directly from the API response via setCodeSecurityConfigurationState, rather than only setting configuration_id. Enterprise Update also captures the API response instead of discarding it. --- ...ithub_enterprise_security_configuration.go | 10 +++++++--- ...hub_organization_security_configuration.go | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 51fbe731b7..203e23d8f4 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -292,8 +292,8 @@ func resourceGithubEnterpriseSecurityConfigurationCreate(ctx context.Context, d } d.SetId(id) - if err = d.Set("configuration_id", int(configuration.GetID())); err != nil { - return diag.FromErr(err) + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { + return diags } tflog.Info(ctx, "Created enterprise code security configuration", map[string]any{ @@ -360,7 +360,7 @@ func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d config := expandCodeSecurityConfigurationCommon(d) - _, _, err := client.Enterprise.UpdateCodeSecurityConfiguration(ctx, enterprise, id, config) + configuration, _, err := client.Enterprise.UpdateCodeSecurityConfiguration(ctx, enterprise, id, config) if err != nil { tflog.Error(ctx, "Failed to update enterprise code security configuration", map[string]any{ "enterprise": enterprise, @@ -370,6 +370,10 @@ func resourceGithubEnterpriseSecurityConfigurationUpdate(ctx context.Context, d return diag.FromErr(err) } + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { + return diags + } + tflog.Info(ctx, "Updated enterprise code security configuration", map[string]any{ "enterprise": enterprise, "id": id, diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index 3b24942eaa..a6a55392a7 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -330,7 +330,13 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, } d.SetId(id) - if err = d.Set("configuration_id", int(configuration.GetID())); err != nil { + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { + return diags + } + if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { return diag.FromErr(err) } @@ -413,7 +419,7 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, config := expandCodeSecurityConfigurationCommon(d) expandSecretScanningDelegatedBypass(d, &config) - _, _, err = client.Organizations.UpdateCodeSecurityConfiguration(ctx, org, id, config) + configuration, _, err := client.Organizations.UpdateCodeSecurityConfiguration(ctx, org, id, config) if err != nil { tflog.Error(ctx, "Failed to update organization code security configuration", map[string]any{ "organization": org, @@ -423,6 +429,16 @@ func resourceGithubOrganizationSecurityConfigurationUpdate(ctx context.Context, return diag.FromErr(err) } + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { + return diags + } + if err = d.Set("secret_scanning_delegated_bypass", configuration.GetSecretScanningDelegatedBypass()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("secret_scanning_delegated_bypass_options", flattenSecretScanningDelegatedBypassOptions(configuration.SecretScanningDelegatedBypassOptions)); err != nil { + return diag.FromErr(err) + } + tflog.Info(ctx, "Updated organization code security configuration", map[string]any{ "organization": org, "id": id, From 67bd694882729a54dc4faae7c51c22d853b59f5f Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:39:56 +1100 Subject: [PATCH 08/12] fix: add CheckDestroy, fix configuration_id type, fix code_security description - Add CheckDestroy functions to all acceptance tests for both org and enterprise security configuration resources - Cast configuration.GetID() to int to match schema.TypeInt - Fix redundant "code security configuration for the code security configuration" description on the code_security field --- ...ithub_enterprise_security_configuration.go | 2 +- ..._enterprise_security_configuration_test.go | 36 ++++++++++++++++++ ...hub_organization_security_configuration.go | 2 +- ...rganization_security_configuration_test.go | 37 +++++++++++++++++++ github/util_security_configuration.go | 2 +- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 203e23d8f4..31ee90cd19 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -169,7 +169,7 @@ func resourceGithubEnterpriseSecurityConfiguration() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - Description: "The code security configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + Description: "The code security setting. Can be one of 'enabled', 'disabled', 'not_set'.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ "enabled", "disabled", "not_set", }, false)), diff --git a/github/resource_github_enterprise_security_configuration_test.go b/github/resource_github_enterprise_security_configuration_test.go index 24023a7e16..6e4ce1d2e4 100644 --- a/github/resource_github_enterprise_security_configuration_test.go +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -1,13 +1,16 @@ package github import ( + "context" "fmt" + "strconv" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -35,6 +38,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -69,6 +73,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -106,6 +111,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: configBefore, @@ -155,6 +161,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -187,6 +194,7 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -201,3 +209,31 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { }) }) } + +func testAccCheckGithubEnterpriseSecurityConfigurationDestroy(s *terraform.State) error { + meta, err := getTestMeta() + if err != nil { + return err + } + conn := meta.v3client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_enterprise_security_configuration" { + continue + } + enterpriseSlug := rs.Primary.Attributes["enterprise_slug"] + configIDStr := rs.Primary.Attributes["configuration_id"] + configID, err := strconv.ParseInt(configIDStr, 10, 64) + if err != nil { + return err + } + _, resp, err := conn.Enterprise.GetCodeSecurityConfiguration(context.Background(), enterpriseSlug, configID) + if err == nil { + return fmt.Errorf("enterprise security configuration %s still exists", configIDStr) + } + if resp.StatusCode != 404 { + return err + } + } + return nil +} diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index a6a55392a7..2a075ec38d 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -163,7 +163,7 @@ func resourceGithubOrganizationSecurityConfiguration() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - Description: "The code security configuration for the code security configuration. Can be one of 'enabled', 'disabled', 'not_set'.", + Description: "The code security setting. Can be one of 'enabled', 'disabled', 'not_set'.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ "enabled", "disabled", "not_set", }, false)), diff --git a/github/resource_github_organization_security_configuration_test.go b/github/resource_github_organization_security_configuration_test.go index 11fd8c50cd..9534012885 100644 --- a/github/resource_github_organization_security_configuration_test.go +++ b/github/resource_github_organization_security_configuration_test.go @@ -1,13 +1,16 @@ package github import ( + "context" "fmt" + "strconv" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -34,6 +37,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -67,6 +71,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -102,6 +107,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: configBefore, @@ -151,6 +157,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -182,6 +189,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -219,6 +227,7 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, Steps: []resource.TestStep{ { Config: config, @@ -233,3 +242,31 @@ func TestAccGithubOrganizationSecurityConfiguration(t *testing.T) { }) }) } + +func testAccCheckGithubOrganizationSecurityConfigurationDestroy(s *terraform.State) error { + meta, err := getTestMeta() + if err != nil { + return err + } + conn := meta.v3client + orgName := meta.name + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_organization_security_configuration" { + continue + } + configIDStr := rs.Primary.Attributes["configuration_id"] + configID, err := strconv.ParseInt(configIDStr, 10, 64) + if err != nil { + return err + } + _, resp, err := conn.Organizations.GetCodeSecurityConfiguration(context.Background(), orgName, configID) + if err == nil { + return fmt.Errorf("organization security configuration %s still exists", configIDStr) + } + if resp.StatusCode != 404 { + return err + } + } + return nil +} diff --git a/github/util_security_configuration.go b/github/util_security_configuration.go index 42187f5726..cc31502752 100644 --- a/github/util_security_configuration.go +++ b/github/util_security_configuration.go @@ -67,7 +67,7 @@ func flattenSecretScanningDelegatedBypassOptions(options *github.SecretScanningD // setCodeSecurityConfigurationState writes all shared CodeSecurityConfiguration fields to Terraform state. // Used by both the organization and enterprise security configuration resources. func setCodeSecurityConfigurationState(d *schema.ResourceData, configuration *github.CodeSecurityConfiguration) diag.Diagnostics { - if err := d.Set("configuration_id", configuration.GetID()); err != nil { + if err := d.Set("configuration_id", int(configuration.GetID())); err != nil { return diag.FromErr(err) } if err := d.Set("name", configuration.Name); err != nil { From f8abb8fe4e3403c91913041159eaded919099058 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:09:46 +1100 Subject: [PATCH 09/12] fix: address PR #3284 review feedback from deiga (round 2) Use single template string in enterprise update test, remove superfluous buildID/SetId in import functions, refactor util tests to table-driven arrays. --- ...ithub_enterprise_security_configuration.go | 6 - ..._enterprise_security_configuration_test.go | 18 +- ...hub_organization_security_configuration.go | 8 +- github/util_security_configuration_test.go | 416 ++++++++++-------- 4 files changed, 247 insertions(+), 201 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index 31ee90cd19..f23049ea7f 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -429,12 +429,6 @@ func resourceGithubEnterpriseSecurityConfigurationImport(_ context.Context, d *s return nil, fmt.Errorf("invalid configuration_id %q: %w", configIDStr, err) } - id, err := buildID(enterpriseSlug, configIDStr) - if err != nil { - return nil, err - } - d.SetId(id) - if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { return nil, err } diff --git a/github/resource_github_enterprise_security_configuration_test.go b/github/resource_github_enterprise_security_configuration_test.go index 6e4ce1d2e4..3c51b2200d 100644 --- a/github/resource_github_enterprise_security_configuration_test.go +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -92,21 +92,15 @@ func TestAccGithubEnterpriseSecurityConfiguration(t *testing.T) { configName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) configNameUpdated := fmt.Sprintf("%supdated-%s", testResourcePrefix, randomID) - configBefore := fmt.Sprintf(` + tmpl := ` resource "github_enterprise_security_configuration" "test" { enterprise_slug = "%s" name = "%s" - description = "Test configuration" - advanced_security = "disabled" - }`, testAccConf.enterpriseSlug, configName) - - configAfter := fmt.Sprintf(` - resource "github_enterprise_security_configuration" "test" { - enterprise_slug = "%s" - name = "%s" - description = "Test configuration updated" - advanced_security = "enabled" - }`, testAccConf.enterpriseSlug, configNameUpdated) + description = "%s" + advanced_security = "%s" + }` + configBefore := fmt.Sprintf(tmpl, testAccConf.enterpriseSlug, configName, "Test configuration", "disabled") + configAfter := fmt.Sprintf(tmpl, testAccConf.enterpriseSlug, configNameUpdated, "Test configuration updated", "enabled") resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessEnterprise(t) }, diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index 2a075ec38d..bc9ca312e3 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -488,7 +488,7 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, } func resourceGithubOrganizationSecurityConfigurationImport(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { - orgName, configIDStr, err := parseID2(d.Id()) + _, configIDStr, err := parseID2(d.Id()) if err != nil { return nil, fmt.Errorf("invalid import specified: supplied import must be written as :. Parse error: %w", err) } @@ -498,12 +498,6 @@ func resourceGithubOrganizationSecurityConfigurationImport(_ context.Context, d return nil, fmt.Errorf("invalid configuration_id %q: %w", configIDStr, err) } - id, err := buildID(orgName, configIDStr) - if err != nil { - return nil, err - } - d.SetId(id) - if err = d.Set("configuration_id", int(configID)); err != nil { return nil, err } diff --git a/github/util_security_configuration_test.go b/github/util_security_configuration_test.go index 1f996bfd5b..5d95ec7d50 100644 --- a/github/util_security_configuration_test.go +++ b/github/util_security_configuration_test.go @@ -7,193 +7,257 @@ import ( ) func TestFlattenDependencyGraphAutosubmitActionOptions(t *testing.T) { - t.Run("returns empty slice when options is nil", func(t *testing.T) { - result := flattenDependencyGraphAutosubmitActionOptions(nil) - if len(result) != 0 { - t.Errorf("expected empty slice, got %v", result) - } - }) - - t.Run("omits labeled_runners key when LabeledRunners is nil", func(t *testing.T) { - opts := &github.DependencyGraphAutosubmitActionOptions{} - result := flattenDependencyGraphAutosubmitActionOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if _, ok := m["labeled_runners"]; ok { - t.Errorf("labeled_runners should be absent when LabeledRunners is nil") - } - }) + tests := []struct { + name string + input *github.DependencyGraphAutosubmitActionOptions + expect func(t *testing.T, result []any) + }{ + { + name: "returns empty slice when options is nil", + input: nil, + expect: func(t *testing.T, result []any) { + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }, + }, + { + name: "omits labeled_runners key when LabeledRunners is nil", + input: &github.DependencyGraphAutosubmitActionOptions{}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["labeled_runners"]; ok { + t.Errorf("labeled_runners should be absent when LabeledRunners is nil") + } + }, + }, + { + name: "sets labeled_runners when LabeledRunners is non-nil", + input: &github.DependencyGraphAutosubmitActionOptions{LabeledRunners: github.Ptr(true)}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["labeled_runners"] != true { + t.Errorf("expected labeled_runners true, got %v", m["labeled_runners"]) + } + }, + }, + } - t.Run("sets labeled_runners when LabeledRunners is non-nil", func(t *testing.T) { - opts := &github.DependencyGraphAutosubmitActionOptions{ - LabeledRunners: github.Ptr(true), - } - result := flattenDependencyGraphAutosubmitActionOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if m["labeled_runners"] != true { - t.Errorf("expected labeled_runners true, got %v", m["labeled_runners"]) - } - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flattenDependencyGraphAutosubmitActionOptions(tt.input) + tt.expect(t, result) + }) + } } func TestFlattenCodeScanningOptions(t *testing.T) { - t.Run("returns empty slice when options is nil", func(t *testing.T) { - result := flattenCodeScanningOptions(nil) - if len(result) != 0 { - t.Errorf("expected empty slice, got %v", result) - } - }) - - t.Run("omits allow_advanced key when AllowAdvanced is nil", func(t *testing.T) { - opts := &github.CodeScanningOptions{} - result := flattenCodeScanningOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if _, ok := m["allow_advanced"]; ok { - t.Errorf("allow_advanced should be absent when AllowAdvanced is nil") - } - }) - - t.Run("sets allow_advanced when AllowAdvanced is true", func(t *testing.T) { - opts := &github.CodeScanningOptions{ - AllowAdvanced: github.Ptr(true), - } - result := flattenCodeScanningOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if m["allow_advanced"] != true { - t.Errorf("expected allow_advanced true, got %v", m["allow_advanced"]) - } - }) + tests := []struct { + name string + input *github.CodeScanningOptions + expect func(t *testing.T, result []any) + }{ + { + name: "returns empty slice when options is nil", + input: nil, + expect: func(t *testing.T, result []any) { + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }, + }, + { + name: "omits allow_advanced key when AllowAdvanced is nil", + input: &github.CodeScanningOptions{}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["allow_advanced"]; ok { + t.Errorf("allow_advanced should be absent when AllowAdvanced is nil") + } + }, + }, + { + name: "sets allow_advanced when AllowAdvanced is true", + input: &github.CodeScanningOptions{AllowAdvanced: github.Ptr(true)}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["allow_advanced"] != true { + t.Errorf("expected allow_advanced true, got %v", m["allow_advanced"]) + } + }, + }, + { + name: "sets allow_advanced when AllowAdvanced is false", + input: &github.CodeScanningOptions{AllowAdvanced: github.Ptr(false)}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["allow_advanced"] != false { + t.Errorf("expected allow_advanced false, got %v", m["allow_advanced"]) + } + }, + }, + } - t.Run("sets allow_advanced when AllowAdvanced is false", func(t *testing.T) { - opts := &github.CodeScanningOptions{ - AllowAdvanced: github.Ptr(false), - } - result := flattenCodeScanningOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if m["allow_advanced"] != false { - t.Errorf("expected allow_advanced false, got %v", m["allow_advanced"]) - } - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flattenCodeScanningOptions(tt.input) + tt.expect(t, result) + }) + } } func TestFlattenSecretScanningDelegatedBypassOptions(t *testing.T) { - t.Run("returns empty slice when options is nil", func(t *testing.T) { - result := flattenSecretScanningDelegatedBypassOptions(nil) - if len(result) != 0 { - t.Errorf("expected empty slice, got %v", result) - } - }) - - t.Run("omits reviewers key when Reviewers is nil", func(t *testing.T) { - opts := &github.SecretScanningDelegatedBypassOptions{} - result := flattenSecretScanningDelegatedBypassOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if _, ok := m["reviewers"]; ok { - t.Errorf("reviewers should be absent when Reviewers is nil") - } - }) + tests := []struct { + name string + input *github.SecretScanningDelegatedBypassOptions + expect func(t *testing.T, result []any) + }{ + { + name: "returns empty slice when options is nil", + input: nil, + expect: func(t *testing.T, result []any) { + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }, + }, + { + name: "omits reviewers key when Reviewers is nil", + input: &github.SecretScanningDelegatedBypassOptions{}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["reviewers"]; ok { + t.Errorf("reviewers should be absent when Reviewers is nil") + } + }, + }, + { + name: "sets reviewers when Reviewers is populated", + input: &github.SecretScanningDelegatedBypassOptions{ + Reviewers: []*github.BypassReviewer{ + {ReviewerID: 42, ReviewerType: "TEAM"}, + {ReviewerID: 99, ReviewerType: "ROLE"}, + }, + }, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + reviewers, ok := m["reviewers"].([]any) + if !ok { + t.Fatalf("expected reviewers to be []any, got %T", m["reviewers"]) + } + if len(reviewers) != 2 { + t.Fatalf("expected 2 reviewers, got %d", len(reviewers)) + } + first := reviewers[0].(map[string]any) + if first["reviewer_id"] != int64(42) { + t.Errorf("expected reviewer_id 42, got %v", first["reviewer_id"]) + } + if first["reviewer_type"] != "TEAM" { + t.Errorf("expected reviewer_type TEAM, got %v", first["reviewer_type"]) + } + second := reviewers[1].(map[string]any) + if second["reviewer_id"] != int64(99) { + t.Errorf("expected reviewer_id 99, got %v", second["reviewer_id"]) + } + if second["reviewer_type"] != "ROLE" { + t.Errorf("expected reviewer_type ROLE, got %v", second["reviewer_type"]) + } + }, + }, + } - t.Run("sets reviewers when Reviewers is populated", func(t *testing.T) { - opts := &github.SecretScanningDelegatedBypassOptions{ - Reviewers: []*github.BypassReviewer{ - {ReviewerID: 42, ReviewerType: "TEAM"}, - {ReviewerID: 99, ReviewerType: "ROLE"}, - }, - } - result := flattenSecretScanningDelegatedBypassOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - reviewers, ok := m["reviewers"].([]any) - if !ok { - t.Fatalf("expected reviewers to be []any, got %T", m["reviewers"]) - } - if len(reviewers) != 2 { - t.Fatalf("expected 2 reviewers, got %d", len(reviewers)) - } - first := reviewers[0].(map[string]any) - if first["reviewer_id"] != int64(42) { - t.Errorf("expected reviewer_id 42, got %v", first["reviewer_id"]) - } - if first["reviewer_type"] != "TEAM" { - t.Errorf("expected reviewer_type TEAM, got %v", first["reviewer_type"]) - } - second := reviewers[1].(map[string]any) - if second["reviewer_id"] != int64(99) { - t.Errorf("expected reviewer_id 99, got %v", second["reviewer_id"]) - } - if second["reviewer_type"] != "ROLE" { - t.Errorf("expected reviewer_type ROLE, got %v", second["reviewer_type"]) - } - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flattenSecretScanningDelegatedBypassOptions(tt.input) + tt.expect(t, result) + }) + } } func TestFlattenCodeScanningDefaultSetupOptions(t *testing.T) { - t.Run("returns empty slice when options is nil", func(t *testing.T) { - result := flattenCodeScanningDefaultSetupOptions(nil) - if len(result) != 0 { - t.Errorf("expected empty slice, got %v", result) - } - }) - - t.Run("omits runner_type key when RunnerType is empty string", func(t *testing.T) { - opts := &github.CodeScanningDefaultSetupOptions{ - RunnerType: "", - } - result := flattenCodeScanningDefaultSetupOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if _, ok := m["runner_type"]; ok { - t.Errorf("runner_type should be absent when RunnerType is empty, got %q", m["runner_type"]) - } - }) - - t.Run("sets runner_type when RunnerType is non-empty", func(t *testing.T) { - opts := &github.CodeScanningDefaultSetupOptions{ - RunnerType: "standard", - } - result := flattenCodeScanningDefaultSetupOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if m["runner_type"] != "standard" { - t.Errorf("expected runner_type %q, got %q", "standard", m["runner_type"]) - } - }) + tests := []struct { + name string + input *github.CodeScanningDefaultSetupOptions + expect func(t *testing.T, result []any) + }{ + { + name: "returns empty slice when options is nil", + input: nil, + expect: func(t *testing.T, result []any) { + if len(result) != 0 { + t.Errorf("expected empty slice, got %v", result) + } + }, + }, + { + name: "omits runner_type key when RunnerType is empty string", + input: &github.CodeScanningDefaultSetupOptions{RunnerType: ""}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if _, ok := m["runner_type"]; ok { + t.Errorf("runner_type should be absent when RunnerType is empty, got %q", m["runner_type"]) + } + }, + }, + { + name: "sets runner_type when RunnerType is non-empty", + input: &github.CodeScanningDefaultSetupOptions{RunnerType: "standard"}, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["runner_type"] != "standard" { + t.Errorf("expected runner_type %q, got %q", "standard", m["runner_type"]) + } + }, + }, + { + name: "sets runner_label when RunnerLabel is non-nil", + input: &github.CodeScanningDefaultSetupOptions{ + RunnerType: "labeled", + RunnerLabel: github.Ptr("my-runner"), + }, + expect: func(t *testing.T, result []any) { + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + m := result[0].(map[string]any) + if m["runner_label"] != "my-runner" { + t.Errorf("expected runner_label %q, got %q", "my-runner", m["runner_label"]) + } + }, + }, + } - t.Run("sets runner_label when RunnerLabel is non-nil", func(t *testing.T) { - opts := &github.CodeScanningDefaultSetupOptions{ - RunnerType: "labeled", - RunnerLabel: github.Ptr("my-runner"), - } - result := flattenCodeScanningDefaultSetupOptions(opts) - if len(result) != 1 { - t.Fatalf("expected 1 element, got %d", len(result)) - } - m := result[0].(map[string]any) - if m["runner_label"] != "my-runner" { - t.Errorf("expected runner_label %q, got %q", "my-runner", m["runner_label"]) - } - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flattenCodeScanningDefaultSetupOptions(tt.input) + tt.expect(t, result) + }) + } } From dadd5b46353cb47bb0dd92fa2a985edc4fd2baf0 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:53:06 +1100 Subject: [PATCH 10/12] test: add unit tests for expand functions in util_security_configuration Adds table-driven tests for expandCodeSecurityConfigurationCommon and expandSecretScanningDelegatedBypass, covering minimal input, all string fields, nested block options, and delegated bypass with reviewers. --- github/util_security_configuration_test.go | 245 +++++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/github/util_security_configuration_test.go b/github/util_security_configuration_test.go index 5d95ec7d50..051f5f1c7d 100644 --- a/github/util_security_configuration_test.go +++ b/github/util_security_configuration_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func TestFlattenDependencyGraphAutosubmitActionOptions(t *testing.T) { @@ -261,3 +262,247 @@ func TestFlattenCodeScanningDefaultSetupOptions(t *testing.T) { }) } } + +func TestExpandCodeSecurityConfigurationCommon(t *testing.T) { + resourceSchema := resourceGithubOrganizationSecurityConfiguration().Schema + + tests := []struct { + name string + input map[string]any + expect func(t *testing.T, config github.CodeSecurityConfiguration) + }{ + { + name: "minimal input sets only name", + input: map[string]any{ + "name": "my-config", + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.Name != "my-config" { + t.Errorf("expected name %q, got %q", "my-config", config.Name) + } + if config.AdvancedSecurity != nil { + t.Errorf("expected AdvancedSecurity nil, got %v", *config.AdvancedSecurity) + } + if config.DependencyGraph != nil { + t.Errorf("expected DependencyGraph nil, got %v", *config.DependencyGraph) + } + if config.Enforcement != nil { + t.Errorf("expected Enforcement nil, got %v", *config.Enforcement) + } + }, + }, + { + name: "sets all string fields", + input: map[string]any{ + "name": "full-config", + "description": "A test config", + "advanced_security": "enabled", + "dependency_graph": "enabled", + "dependency_graph_autosubmit_action": "enabled", + "dependabot_alerts": "enabled", + "dependabot_security_updates": "disabled", + "code_scanning_default_setup": "enabled", + "code_scanning_delegated_alert_dismissal": "not_set", + "code_security": "enabled", + "secret_scanning": "enabled", + "secret_scanning_push_protection": "enabled", + "secret_scanning_validity_checks": "disabled", + "secret_scanning_non_provider_patterns": "not_set", + "secret_scanning_generic_secrets": "disabled", + "secret_scanning_delegated_alert_dismissal": "not_set", + "secret_protection": "enabled", + "private_vulnerability_reporting": "enabled", + "enforcement": "enforced", + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.Name != "full-config" { + t.Errorf("expected name %q, got %q", "full-config", config.Name) + } + if config.Description != "A test config" { + t.Errorf("expected description %q, got %q", "A test config", config.Description) + } + if config.GetAdvancedSecurity() != "enabled" { + t.Errorf("expected AdvancedSecurity %q, got %q", "enabled", config.GetAdvancedSecurity()) + } + if config.GetDependencyGraph() != "enabled" { + t.Errorf("expected DependencyGraph %q, got %q", "enabled", config.GetDependencyGraph()) + } + if config.GetDependabotSecurityUpdates() != "disabled" { + t.Errorf("expected DependabotSecurityUpdates %q, got %q", "disabled", config.GetDependabotSecurityUpdates()) + } + if config.GetEnforcement() != "enforced" { + t.Errorf("expected Enforcement %q, got %q", "enforced", config.GetEnforcement()) + } + if config.GetSecretScanning() != "enabled" { + t.Errorf("expected SecretScanning %q, got %q", "enabled", config.GetSecretScanning()) + } + if config.GetPrivateVulnerabilityReporting() != "enabled" { + t.Errorf("expected PrivateVulnerabilityReporting %q, got %q", "enabled", config.GetPrivateVulnerabilityReporting()) + } + }, + }, + { + name: "sets dependency_graph_autosubmit_action_options", + input: map[string]any{ + "name": "with-autosubmit-opts", + "dependency_graph_autosubmit_action_options": []any{ + map[string]any{ + "labeled_runners": true, + }, + }, + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.DependencyGraphAutosubmitActionOptions == nil { + t.Fatal("expected DependencyGraphAutosubmitActionOptions to be set") + } + if !config.DependencyGraphAutosubmitActionOptions.GetLabeledRunners() { + t.Errorf("expected LabeledRunners true, got false") + } + }, + }, + { + name: "sets code_scanning_default_setup_options with runner_label", + input: map[string]any{ + "name": "with-setup-opts", + "code_scanning_default_setup_options": []any{ + map[string]any{ + "runner_type": "labeled", + "runner_label": "my-runner", + }, + }, + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.CodeScanningDefaultSetupOptions == nil { + t.Fatal("expected CodeScanningDefaultSetupOptions to be set") + } + if config.CodeScanningDefaultSetupOptions.RunnerType != "labeled" { + t.Errorf("expected RunnerType %q, got %q", "labeled", config.CodeScanningDefaultSetupOptions.RunnerType) + } + if config.CodeScanningDefaultSetupOptions.GetRunnerLabel() != "my-runner" { + t.Errorf("expected RunnerLabel %q, got %q", "my-runner", config.CodeScanningDefaultSetupOptions.GetRunnerLabel()) + } + }, + }, + { + name: "sets code_scanning_options", + input: map[string]any{ + "name": "with-scan-opts", + "code_scanning_options": []any{ + map[string]any{ + "allow_advanced": true, + }, + }, + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.CodeScanningOptions == nil { + t.Fatal("expected CodeScanningOptions to be set") + } + if !config.CodeScanningOptions.GetAllowAdvanced() { + t.Errorf("expected AllowAdvanced true, got false") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := schema.TestResourceDataRaw(t, resourceSchema, tt.input) + result := expandCodeSecurityConfigurationCommon(d) + tt.expect(t, result) + }) + } +} + +func TestExpandSecretScanningDelegatedBypass(t *testing.T) { + resourceSchema := resourceGithubOrganizationSecurityConfiguration().Schema + + tests := []struct { + name string + input map[string]any + expect func(t *testing.T, config github.CodeSecurityConfiguration) + }{ + { + name: "no bypass fields leaves config unchanged", + input: map[string]any{ + "name": "no-bypass", + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.SecretScanningDelegatedBypass != nil { + t.Errorf("expected SecretScanningDelegatedBypass nil, got %v", *config.SecretScanningDelegatedBypass) + } + if config.SecretScanningDelegatedBypassOptions != nil { + t.Errorf("expected SecretScanningDelegatedBypassOptions nil, got %v", config.SecretScanningDelegatedBypassOptions) + } + }, + }, + { + name: "sets bypass string without options", + input: map[string]any{ + "name": "bypass-only", + "secret_scanning_delegated_bypass": "enabled", + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.GetSecretScanningDelegatedBypass() != "enabled" { + t.Errorf("expected SecretScanningDelegatedBypass %q, got %q", "enabled", config.GetSecretScanningDelegatedBypass()) + } + if config.SecretScanningDelegatedBypassOptions != nil { + t.Errorf("expected SecretScanningDelegatedBypassOptions nil, got %v", config.SecretScanningDelegatedBypassOptions) + } + }, + }, + { + name: "sets bypass with reviewers", + input: map[string]any{ + "name": "bypass-with-reviewers", + "secret_scanning_delegated_bypass": "enabled", + "secret_scanning_delegated_bypass_options": []any{ + map[string]any{ + "reviewers": []any{ + map[string]any{ + "reviewer_id": 42, + "reviewer_type": "TEAM", + }, + map[string]any{ + "reviewer_id": 99, + "reviewer_type": "ROLE", + }, + }, + }, + }, + }, + expect: func(t *testing.T, config github.CodeSecurityConfiguration) { + if config.GetSecretScanningDelegatedBypass() != "enabled" { + t.Errorf("expected SecretScanningDelegatedBypass %q, got %q", "enabled", config.GetSecretScanningDelegatedBypass()) + } + if config.SecretScanningDelegatedBypassOptions == nil { + t.Fatal("expected SecretScanningDelegatedBypassOptions to be set") + } + reviewers := config.SecretScanningDelegatedBypassOptions.Reviewers + if len(reviewers) != 2 { + t.Fatalf("expected 2 reviewers, got %d", len(reviewers)) + } + if reviewers[0].ReviewerID != 42 { + t.Errorf("expected first reviewer_id 42, got %d", reviewers[0].ReviewerID) + } + if reviewers[0].ReviewerType != "TEAM" { + t.Errorf("expected first reviewer_type %q, got %q", "TEAM", reviewers[0].ReviewerType) + } + if reviewers[1].ReviewerID != 99 { + t.Errorf("expected second reviewer_id 99, got %d", reviewers[1].ReviewerID) + } + if reviewers[1].ReviewerType != "ROLE" { + t.Errorf("expected second reviewer_type %q, got %q", "ROLE", reviewers[1].ReviewerType) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := schema.TestResourceDataRaw(t, resourceSchema, tt.input) + config := github.CodeSecurityConfiguration{Name: d.Get("name").(string)} + expandSecretScanningDelegatedBypass(d, &config) + tt.expect(t, config) + }) + } +} From f6a71856aa6acaf1ac29438c6dd6aef655082b52 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:14:59 +1000 Subject: [PATCH 11/12] fix: simplify resource ID to plain numeric config ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the org name from the compound resource ID since it was never used — Read/Update/Delete all get the org from meta.(*Owner).name. This aligns with the convention used by other org-scoped resources (organization_ruleset, organization_webhook, organization_custom_role, etc.). --- ..._github_organization_security_configuration.go | 15 +++------------ ...anization_security_configuration.html.markdown | 4 ++-- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index bc9ca312e3..8ad758ca94 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -324,11 +324,7 @@ func resourceGithubOrganizationSecurityConfigurationCreate(ctx context.Context, return diag.FromErr(err) } - id, err := buildID(org, strconv.FormatInt(configuration.GetID(), 10)) - if err != nil { - return diag.FromErr(err) - } - d.SetId(id) + d.SetId(strconv.FormatInt(configuration.GetID(), 10)) if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { return diags @@ -488,14 +484,9 @@ func resourceGithubOrganizationSecurityConfigurationDelete(ctx context.Context, } func resourceGithubOrganizationSecurityConfigurationImport(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { - _, configIDStr, err := parseID2(d.Id()) - if err != nil { - return nil, fmt.Errorf("invalid import specified: supplied import must be written as :. Parse error: %w", err) - } - - configID, err := strconv.ParseInt(configIDStr, 10, 64) + configID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return nil, fmt.Errorf("invalid configuration_id %q: %w", configIDStr, err) + return nil, fmt.Errorf("invalid configuration_id %q: %w", d.Id(), err) } if err = d.Set("configuration_id", int(configID)); err != nil { diff --git a/website/docs/r/organization_security_configuration.html.markdown b/website/docs/r/organization_security_configuration.html.markdown index 9236eb3481..34f5186748 100644 --- a/website/docs/r/organization_security_configuration.html.markdown +++ b/website/docs/r/organization_security_configuration.html.markdown @@ -90,8 +90,8 @@ The `secret_scanning_delegated_bypass_options` block supports: ## Import -GitHub Organization Code Security Configurations can be imported using the organization name and the configuration ID separated by a colon, e.g. +GitHub Organization Code Security Configurations can be imported using the configuration ID, e.g. ```text -$ terraform import github_organization_security_configuration.example my-org:123 +$ terraform import github_organization_security_configuration.example 123 ``` From 8a4e18b3d25f6247452a0dfc39331b65c0a66563 Mon Sep 17 00:00:00 2001 From: sprioriello <116602895+sprioriello@users.noreply.github.com> Date: Mon, 25 May 2026 21:03:39 +1000 Subject: [PATCH 12/12] chore: bump go-github import to v86 in security configuration files Aligns the new security configuration resources with upstream's go-github v86 bump (#3413), so the merged branch builds against the unified module graph. --- github/resource_github_enterprise_security_configuration.go | 2 +- github/resource_github_organization_security_configuration.go | 2 +- github/util_security_configuration.go | 2 +- github/util_security_configuration_test.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/github/resource_github_enterprise_security_configuration.go b/github/resource_github_enterprise_security_configuration.go index f23049ea7f..47bc02ffd0 100644 --- a/github/resource_github_enterprise_security_configuration.go +++ b/github/resource_github_enterprise_security_configuration.go @@ -7,7 +7,7 @@ import ( "net/http" "strconv" - "github.com/google/go-github/v84/github" + "github.com/google/go-github/v86/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" diff --git a/github/resource_github_organization_security_configuration.go b/github/resource_github_organization_security_configuration.go index 8ad758ca94..f17f0f7cb5 100644 --- a/github/resource_github_organization_security_configuration.go +++ b/github/resource_github_organization_security_configuration.go @@ -7,7 +7,7 @@ import ( "net/http" "strconv" - "github.com/google/go-github/v84/github" + "github.com/google/go-github/v86/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" diff --git a/github/util_security_configuration.go b/github/util_security_configuration.go index cc31502752..06080d61df 100644 --- a/github/util_security_configuration.go +++ b/github/util_security_configuration.go @@ -1,7 +1,7 @@ package github import ( - "github.com/google/go-github/v84/github" + "github.com/google/go-github/v86/github" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) diff --git a/github/util_security_configuration_test.go b/github/util_security_configuration_test.go index 051f5f1c7d..f041704b9f 100644 --- a/github/util_security_configuration_test.go +++ b/github/util_security_configuration_test.go @@ -3,7 +3,7 @@ package github import ( "testing" - "github.com/google/go-github/v84/github" + "github.com/google/go-github/v86/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" )