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..f23049ea7f --- /dev/null +++ b/github/resource_github_enterprise_security_configuration.go @@ -0,0 +1,442 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "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" + "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.", + }, + "configuration_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The numeric ID of the code security configuration.", + }, + "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 setting. 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_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, "Creating enterprise code security configuration", map[string]any{ + "enterprise": enterprise, + "name": name, + }) + + config := expandCodeSecurityConfigurationCommon(d) + + configuration, _, err := client.Enterprise.CreateCodeSecurityConfiguration(ctx, enterprise, config) + if err != nil { + tflog.Error(ctx, "Failed to create enterprise code security configuration", 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) + + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { + return diags + } + + tflog.Info(ctx, "Created enterprise code security configuration", map[string]any{ + "enterprise": enterprise, + "name": name, + "id": configuration.GetID(), + }) + + return nil +} + +func resourceGithubEnterpriseSecurityConfigurationRead(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)) + + tflog.Trace(ctx, "Reading enterprise code security configuration", 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, "Removing enterprise code security configuration from state because it no longer exists in GitHub", map[string]any{ + "enterprise": enterprise, + "id": id, + }) + d.SetId("") + return nil + } + } + tflog.Error(ctx, "Failed to read enterprise code security configuration", map[string]any{ + "enterprise": enterprise, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + if diags := setCodeSecurityConfigurationState(d, configuration); diags.HasError() { + return diags + } + + tflog.Trace(ctx, "Successfully read enterprise code security configuration", 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 := d.Get("enterprise_slug").(string) + id := int64(d.Get("configuration_id").(int)) + + tflog.Debug(ctx, "Updating enterprise code security configuration", map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + config := expandCodeSecurityConfigurationCommon(d) + + 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, + "id": id, + "error": err.Error(), + }) + 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, + }) + + return nil +} + +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)) + + tflog.Debug(ctx, "Deleting enterprise code security configuration", 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, "Enterprise code security configuration already deleted", map[string]any{ + "enterprise": enterprise, + "id": id, + }) + return nil + } + tflog.Error(ctx, "Failed to delete enterprise code security configuration", map[string]any{ + "enterprise": enterprise, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + tflog.Info(ctx, "Deleted enterprise code security configuration", map[string]any{ + "enterprise": enterprise, + "id": id, + }) + + return nil +} + +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) + } + + configID, err := strconv.ParseInt(configIDStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid configuration_id %q: %w", configIDStr, err) + } + + if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { + 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 new file mode 100644 index 0000000000..3c51b2200d --- /dev/null +++ b/github/resource_github_enterprise_security_configuration_test.go @@ -0,0 +1,233 @@ +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" +) + +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("%s%s", testResourcePrefix, 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, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, + 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")), + statecheck.ExpectKnownValue("github_enterprise_security_configuration.test", + 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, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + 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("%s%s", testResourcePrefix, randomID) + configNameUpdated := fmt.Sprintf("%supdated-%s", testResourcePrefix, 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, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, + 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("%s%s", testResourcePrefix, 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, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, + 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")), + }, + }, + }, + }) + }) + + t.Run("creates enterprise security configuration with minimal config", 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 = "Minimal test configuration" + }`, testAccConf.enterpriseSlug, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEnterpriseSecurityConfigurationDestroy, + 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()), + }, + }, + }, + }) + }) +} + +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 new file mode 100644 index 0000000000..8ad758ca94 --- /dev/null +++ b/github/resource_github_organization_security_configuration.go @@ -0,0 +1,498 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "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" + "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: resourceGithubOrganizationSecurityConfigurationImport, + }, + + 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, + 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 setting. 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, "Creating organization code security configuration", map[string]any{ + "organization": org, + "name": name, + }) + + config := expandCodeSecurityConfigurationCommon(d) + expandSecretScanningDelegatedBypass(d, &config) + + configuration, _, err := client.Organizations.CreateCodeSecurityConfiguration(ctx, org, config) + if err != nil { + tflog.Error(ctx, "Failed to create organization code security configuration", map[string]any{ + "organization": org, + "name": name, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(configuration.GetID(), 10)) + + 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, "Created organization code security configuration", map[string]any{ + "organization": org, + "name": name, + "id": configuration.GetID(), + }) + + return nil +} + +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 := meta.(*Owner).name + id := int64(d.Get("configuration_id").(int)) + + tflog.Trace(ctx, "Reading organization code security configuration", 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, "Removing organization code security configuration from state because it no longer exists in GitHub", map[string]any{ + "organization": org, + "id": id, + }) + d.SetId("") + return nil + } + } + tflog.Error(ctx, "Failed to read organization code security configuration", map[string]any{ + "organization": org, + "id": id, + "error": err.Error(), + }) + 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.Trace(ctx, "Successfully read organization code security configuration", 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 := meta.(*Owner).name + id := int64(d.Get("configuration_id").(int)) + + tflog.Debug(ctx, "Updating organization code security configuration", map[string]any{ + "organization": org, + "id": id, + }) + + config := expandCodeSecurityConfigurationCommon(d) + expandSecretScanningDelegatedBypass(d, &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, + "id": id, + "error": err.Error(), + }) + 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, + }) + + return nil +} + +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 := meta.(*Owner).name + id := int64(d.Get("configuration_id").(int)) + + tflog.Debug(ctx, "Deleting organization code security configuration", 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, "Organization code security configuration already deleted", map[string]any{ + "organization": org, + "id": id, + }) + return nil + } + tflog.Error(ctx, "Failed to delete organization code security configuration", map[string]any{ + "organization": org, + "id": id, + "error": err.Error(), + }) + return diag.FromErr(err) + } + + tflog.Info(ctx, "Deleted organization code security configuration", map[string]any{ + "organization": org, + "id": id, + }) + + return nil +} + +func resourceGithubOrganizationSecurityConfigurationImport(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + configID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid configuration_id %q: %w", d.Id(), 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_organization_security_configuration_test.go b/github/resource_github_organization_security_configuration_test.go new file mode 100644 index 0000000000..9534012885 --- /dev/null +++ b/github/resource_github_organization_security_configuration_test.go @@ -0,0 +1,272 @@ +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" +) + +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("%s%s", testResourcePrefix, 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, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, + 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")), + statecheck.ExpectKnownValue("github_organization_security_configuration.test", + 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, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + 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("%s%s", testResourcePrefix, randomID) + configNameUpdated := fmt.Sprintf("%supdated-%s", testResourcePrefix, randomID) + + configBefore := fmt.Sprintf(` + resource "github_organization_security_configuration" "test" { + name = "%s" + 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) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, + 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("%s%s", testResourcePrefix, 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, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, + 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")), + }, + }, + }, + }) + }) + + t.Run("creates organization security configuration with minimal config", 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 = "Minimal test configuration" + }`, configName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, + 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()), + }, + }, + }, + }) + }) + + t.Run("creates organization security configuration with delegated bypass options", 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 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, + CheckDestroy: testAccCheckGithubOrganizationSecurityConfigurationDestroy, + 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")), + }, + }, + }, + }) + }) +} + +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 new file mode 100644 index 0000000000..cc31502752 --- /dev/null +++ b/github/util_security_configuration.go @@ -0,0 +1,269 @@ +package github + +import ( + "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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} +} + +// 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", int(configuration.GetID())); 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_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) + } + if err := d.Set("code_security", configuration.GetCodeSecurity()); 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_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) + } + 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 { + 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 { + config := github.CodeSecurityConfiguration{ + Name: d.Get("name").(string), + } + if val, ok := d.GetOk("description"); ok { + config.Description = val.(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_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)), + } + } + } + + 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 { + 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 + } + } +} diff --git a/github/util_security_configuration_test.go b/github/util_security_configuration_test.go new file mode 100644 index 0000000000..051f5f1c7d --- /dev/null +++ b/github/util_security_configuration_test.go @@ -0,0 +1,508 @@ +package github + +import ( + "testing" + + "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestFlattenDependencyGraphAutosubmitActionOptions(t *testing.T) { + 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"]) + } + }, + }, + } + + 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) { + 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"]) + } + }, + }, + } + + 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) { + 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"]) + } + }, + }, + } + + 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) { + 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"]) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := flattenCodeScanningDefaultSetupOptions(tt.input) + tt.expect(t, result) + }) + } +} + +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) + }) + } +} 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..8df70c34a4 --- /dev/null +++ b/website/docs/r/enterprise_security_configuration.html.markdown @@ -0,0 +1,89 @@ +--- +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` - (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_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. + +## 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 +``` 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..34f5186748 --- /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 configuration ID, e.g. + +```text +$ terraform import github_organization_security_configuration.example 123 +```