diff --git a/examples/repository_security_and_analysis/main.tf b/examples/repository_security_and_analysis/main.tf index 8cdbb72452..0814f5e63e 100644 --- a/examples/repository_security_and_analysis/main.tf +++ b/examples/repository_security_and_analysis/main.tf @@ -14,6 +14,9 @@ resource "github_repository" "terraformed" { secret_scanning_push_protection { status = "enabled" } + private_vulnerability_reporting { + status = "enabled" + } } } diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 6f4abaeb62..3c4e24ee42 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -188,6 +188,24 @@ func resourceGithubRepository() *schema.Resource { }, }, }, + "private_vulnerability_reporting": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "The private vulnerability reporting configuration for the repository.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "status": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: toDiagFunc(validation.StringInSlice([]string{"enabled", "disabled"}, false), "private_vulnerability_reporting"), + Description: "Set to 'enabled' to enable private vulnerability reporting on the repository. Can be 'enabled' or 'disabled'.", + }, + }, + }, + }, }, }, }, @@ -568,6 +586,9 @@ func calculateSecurityAndAnalysis(d *schema.ResourceData) *github.SecurityAndAna Status: github.String(status), } } + // Note: private_vulnerability_reporting is handled separately via the + // EnablePrivateReporting/DisablePrivateReporting API, not via the + // repository Edit API's SecurityAndAnalysis field. return &securityAndAnalysis } @@ -773,6 +794,11 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { return err } + err = updatePrivateVulnerabilityReporting(d, client, ctx, owner, repoName) + if err != nil { + return err + } + return resourceGithubRepositoryUpdate(d, meta) } @@ -900,7 +926,38 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta any) error { } } - if err = d.Set("security_and_analysis", flattenSecurityAndAnalysis(repo.GetSecurityAndAnalysis())); err != nil { + securityAndAnalysis := flattenSecurityAndAnalysis(repo.GetSecurityAndAnalysis()) + + // Private Vulnerability Reporting is not returned in the SecurityAndAnalysis + // from the repository API. We need to fetch it separately. + pvrEnabled, resp, err := client.Repositories.IsPrivateReportingEnabled(ctx, owner, repoName) + if err != nil { + // PVR is only available for public repos. A 404 means it's not available (e.g., private repo). + // Any other error should be returned. + if resp == nil || resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("error reading private vulnerability reporting status: %w", err) + } + log.Printf("[DEBUG] Private vulnerability reporting not available for repository %s/%s", owner, repoName) + } else { + // Merge the PVR status into the security_and_analysis map + status := "disabled" + if pvrEnabled { + status = "enabled" + } + // Initialize securityAndAnalysis if it's empty (can happen for private repos) + if len(securityAndAnalysis) == 0 { + securityAndAnalysis = []any{make(map[string]any)} + } + saMap, ok := securityAndAnalysis[0].(map[string]any) + if !ok { + return fmt.Errorf("failed to parse security_and_analysis map") + } + saMap["private_vulnerability_reporting"] = []any{map[string]any{ + "status": status, + }} + } + + if err = d.Set("security_and_analysis", securityAndAnalysis); err != nil { return err } @@ -1007,6 +1064,13 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { } } + if d.HasChange("security_and_analysis") { + err = updatePrivateVulnerabilityReporting(d, client, ctx, owner, repoName) + if err != nil { + return err + } + } + if d.HasChange("visibility") { o, n := d.GetChange("visibility") repoReq.Visibility = github.String(n.(string)) @@ -1218,6 +1282,9 @@ func flattenSecurityAndAnalysis(securityAndAnalysis *github.SecurityAndAnalysis) "status": securityAndAnalysis.GetSecretScanningPushProtection().GetStatus(), }} + // Note: private_vulnerability_reporting is not returned by the repository API + // and is fetched separately via IsPrivateReportingEnabled in the Read function. + return []any{securityAndAnalysisMap} } @@ -1251,3 +1318,31 @@ func updateVulnerabilityAlerts(d *schema.ResourceData, client *github.Client, ct _, err := updateVulnerabilityAlerts(ctx, owner, repoName) return err } + +func updatePrivateVulnerabilityReporting(d *schema.ResourceData, client *github.Client, ctx context.Context, owner, repoName string) error { + value, ok := d.GetOk("security_and_analysis") + if !ok { + return nil + } + + asList := value.([]any) + if len(asList) == 0 || asList[0] == nil { + return nil + } + + lookup := asList[0].(map[string]any) + if pvr, ok := lookup["private_vulnerability_reporting"].([]any); ok && len(pvr) > 0 && pvr[0] != nil { + pvrMap := pvr[0].(map[string]any) + if status, ok := pvrMap["status"].(string); ok { + if status == "enabled" { + _, err := client.Repositories.EnablePrivateReporting(ctx, owner, repoName) + return err + } else if status == "disabled" { + _, err := client.Repositories.DisablePrivateReporting(ctx, owner, repoName) + return err + } + } + } + + return nil +} diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index c4fab6f9d8..aef456619b 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -1236,6 +1236,67 @@ func TestAccGithubRepositorySecurity(t *testing.T) { }) }) }) + + t.Run("manages private vulnerability reporting for a public repository", func(t *testing.T) { + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%s" + description = "A repository created by Terraform to test private vulnerability reporting" + visibility = "public" + security_and_analysis { + secret_scanning { + status = "enabled" + } + secret_scanning_push_protection { + status = "disabled" + } + private_vulnerability_reporting { + status = "enabled" + } + } + } + `, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.secret_scanning.0.status", + "enabled", + ), + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.secret_scanning_push_protection.0.status", + "disabled", + ), + resource.TestCheckResourceAttr( + "github_repository.test", "security_and_analysis.0.private_vulnerability_reporting.0.status", + "enabled", + ), + ) + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) } func TestAccGithubRepositoryVisibility(t *testing.T) { diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 4964a74b22..26b1294107 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -178,6 +178,8 @@ The `security_and_analysis` block supports the following: * `secret_scanning_non_provider_patterns` - (Optional) The secret scanning non-provider patterns configuration for this repository. See [Secret Scanning Non-Provider Patterns Configuration](#secret-scanning-non-provider-patterns-configuration) below for more details. +* `private_vulnerability_reporting` - (Optional) The private vulnerability reporting configuration for the repository. See [Private Vulnerability Reporting Configuration](#private-vulnerability-reporting-configuration) below for details. + #### Advanced Security Configuration #### The `advanced_security` block supports the following: @@ -204,6 +206,10 @@ The `advanced_security` block supports the following: * `status` - (Required) Set to `enabled` to enable secret scanning non-provider patterns on the repository. Can be `enabled` or `disabled`. If set to `enabled`, the repository's visibility must be `public`, `security_and_analysis[0].advanced_security[0].status` must also be set to `enabled`, or your Organization must have split licensing for Advanced security. +#### Private Vulnerability Reporting Configuration #### + +* `status` - (Optional) Set to `enabled` to enable private vulnerability reporting on the repository. Can be `enabled` or `disabled`. This feature allows security researchers to privately report potential security vulnerabilities to repository maintainers. See [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories) for more details. Only available for public repositories. + ### Template Repositories `template` supports the following arguments: