Repository: google/go-github
Pull request #2862 (2023-08-08 20:52:06)

Add support for the security and analysis webhook event
Fixes https://github.com/google/go-github/issues/2861

Adds support for the [Security and Analysis webhook event](https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads#security_and_analysis) which fires whenever security and analysis features are enabled or disabled for a repository.

* [x] Ran `go generate`
* [x] Ran `go test`
* [x] Ran `go vet`
* [x] Signed CLA

google-cla[bot]: Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this [failed invocation](https://github.com/google/go-github/pull/2862/checks?check_run_id=15726921162) of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/google/go-github/pull/2862?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> Merging [#2862](https://app.codecov.io/gh/google/go-github/pull/2862?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (e0ec095) into [master](https://app.codecov.io/gh/google/go-github/commit/899235e0a9d750d6fecf9048a676046d50f9d4a3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (899235e) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #2862   +/-   ##
=======================================
  Coverage   98.06%   98.06%           
=======================================
  Files         138      138           
  Lines       12327    12329    +2     
=======================================
+ Hits        12089    12091    +2     
  Misses        162      162           
  Partials       76       76           
```


| [Files Changed](https://app.codecov.io/gh/google/go-github/pull/2862?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | Coverage Δ | |
|---|---|---|
| [github/event\_types.go](https://app.codecov.io/gh/google/go-github/pull/2862?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2V2ZW50X3R5cGVzLmdv) | `100.00% <ø> (ø)` | |
| [github/messages.go](https://app.codecov.io/gh/google/go-github/pull/2862?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL21lc3NhZ2VzLmdv) | `100.00% <ø> (ø)` | |
| [github/event.go](https://app.codecov.io/gh/google/go-github/pull/2862?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2V2ZW50Lmdv) | `100.00% <100.00%> (ø)` | |


byronwolfman: > Thank you, @byronwolfman ! LGTM.
> 
> Awaiting second LGTM+Approval from any other contributor to this repo before merging.

Thanks, sounds good! And thanks for the clear contributing docs that made it possible to put this together. 🙏 💯 
gmlewis: > Thanks, sounds good! And thanks for the clear contributing docs that made it possible to put this together. 🙏 💯

Thank _YOU_ for reading it!  It would be nice if every PR author read it!!! :joy:
byronwolfman: Gently tagging @jporzucek for review as I see you've done some recent work on security and analysis APIs. 🙏 
gmlewis: Thank you, @jporzucek !
Merging.
gmlewis: cc: @evankanderson - this will also affect #2865. Sorry about that.

Patch:
diff --git a/github/event.go b/github/event.go
index 2349cb77..b5f39ac7 100644
--- a/github/event.go
+++ b/github/event.go
@@ -133,6 +133,8 @@ func (e *Event) ParsePayload() (payload interface{}, err error) {
 		payload = &SecretScanningAlertEvent{}
 	case "SecurityAdvisoryEvent":
 		payload = &SecurityAdvisoryEvent{}
+	case "SecurityAndAnalysisEvent":
+		payload = &SecurityAndAnalysisEvent{}
 	case "StarEvent":
 		payload = &StarEvent{}
 	case "StatusEvent":
diff --git a/github/event_types.go b/github/event_types.go
index a2413447..edbe8f53 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -1325,6 +1325,31 @@ type SecretScanningAlertEvent struct {
 	Installation *Installation `json:"installation,omitempty"`
 }
 
+// SecurityAndAnalysisEvent is triggered when code security and analysis features
+// are enabled or disabled for a repository.
+//
+// GitHub API docs: https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads#security_and_analysis
+type SecurityAndAnalysisEvent struct {
+	Changes      *SecurityAndAnalysisChange `json:"changes,omitempty"`
+	Enterprise   *Enterprise                `json:"enterprise,omitempty"`
+	Installation *Installation              `json:"installation,omitempty"`
+	Organization *Organization              `json:"organization,omitempty"`
+	Repository   *Repository                `json:"repository,omitempty"`
+	Sender       *User                      `json:"sender,omitempty"`
+}
+
+// SecurityAndAnalysisChange represents the changes when security and analysis
+// features are enabled or disabled for a repository.
+type SecurityAndAnalysisChange struct {
+	From *SecurityAndAnalysisChangeFrom `json:"from,omitempty"`
+}
+
+// SecurityAndAnalysisChangeFrom represents which change was made when security
+// and analysis features are enabled or disabled for a repository.
+type SecurityAndAnalysisChangeFrom struct {
+	SecurityAndAnalysis *SecurityAndAnalysis `json:"security_and_analysis,omitempty"`
+}
+
 // StarEvent is triggered when a star is added or removed from a repository.
 // The Webhook event name is "star".
 //
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 3f665ede..b76cc3c0 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -20270,6 +20270,70 @@ func (s *SecurityAndAnalysis) GetSecretScanningPushProtection() *SecretScanningP
 	return s.SecretScanningPushProtection
 }
 
+// GetFrom returns the From field.
+func (s *SecurityAndAnalysisChange) GetFrom() *SecurityAndAnalysisChangeFrom {
+	if s == nil {
+		return nil
+	}
+	return s.From
+}
+
+// GetSecurityAndAnalysis returns the SecurityAndAnalysis field.
+func (s *SecurityAndAnalysisChangeFrom) GetSecurityAndAnalysis() *SecurityAndAnalysis {
+	if s == nil {
+		return nil
+	}
+	return s.SecurityAndAnalysis
+}
+
+// GetChanges returns the Changes field.
+func (s *SecurityAndAnalysisEvent) GetChanges() *SecurityAndAnalysisChange {
+	if s == nil {
+		return nil
+	}
+	return s.Changes
+}
+
+// GetEnterprise returns the Enterprise field.
+func (s *SecurityAndAnalysisEvent) GetEnterprise() *Enterprise {
+	if s == nil {
+		return nil
+	}
+	return s.Enterprise
+}
+
+// GetInstallation returns the Installation field.
+func (s *SecurityAndAnalysisEvent) GetInstallation() *Installation {
+	if s == nil {
+		return nil
+	}
+	return s.Installation
+}
+
+// GetOrganization returns the Organization field.
+func (s *SecurityAndAnalysisEvent) GetOrganization() *Organization {
+	if s == nil {
+		return nil
+	}
+	return s.Organization
+}
+
+// GetRepository returns the Repository field.
+func (s *SecurityAndAnalysisEvent) GetRepository() *Repository {
+	if s == nil {
+		return nil
+	}
+	return s.Repository
+}
+
+// GetSender returns the Sender field.
+func (s *SecurityAndAnalysisEvent) GetSender() *User {
+	if s == nil {
+		return nil
+	}
+	return s.Sender
+}
+
 // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
 func (s *SelectedReposList) GetTotalCount() int {
 	if s == nil || s.TotalCount == nil {
diff --git a/github/messages.go b/github/messages.go
index 929587d3..d6218268 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -96,6 +96,7 @@ var (
 		"release":                        "ReleaseEvent",
 		"secret_scanning_alert":          "SecretScanningAlertEvent",
 		"security_advisory":              "SecurityAdvisoryEvent",
+		"security_and_analysis":          "SecurityAndAnalysisEvent",
 		"star":                           "StarEvent",
 		"status":                         "StatusEvent",
 		"team":                           "TeamEvent",


Test patch:
diff --git a/github/event_types_test.go b/github/event_types_test.go
index 8d6a8b95..3703d454 100644
--- a/github/event_types_test.go
+++ b/github/event_types_test.go
@@ -15808,6 +15808,341 @@ func TestSecurityAdvisoryEvent_Marshal(t *testing.T) {
 	testJSONMarshal(t, u, want)
 }
 
+func TestSecurityAndAnalysisEvent_Marshal(t *testing.T) {
+	testJSONMarshal(t, &SecurityAndAnalysisEvent{}, "{}")
+
+	u := &SecurityAndAnalysisEvent{
+		Changes: &SecurityAndAnalysisChange{
+			From: &SecurityAndAnalysisChangeFrom{
+				SecurityAndAnalysis: &SecurityAndAnalysis{
+					AdvancedSecurity: &AdvancedSecurity{
+						Status: String("enabled"),
+					},
+					SecretScanning: &SecretScanning{
+						Status: String("enabled"),
+					},
+					SecretScanningPushProtection: &SecretScanningPushProtection{
+						Status: String("enabled"),
+					},
+					DependabotSecurityUpdates: &DependabotSecurityUpdates{
+						Status: String("enabled"),
+					},
+				},
+			},
+		},
+		Enterprise: &Enterprise{
+			ID:          Int(1),
+			Slug:        String("s"),
+			Name:        String("n"),
+			NodeID:      String("nid"),
+			AvatarURL:   String("au"),
+			Description: String("d"),
+			WebsiteURL:  String("wu"),
+			HTMLURL:     String("hu"),
+			CreatedAt:   &Timestamp{referenceTime},
+			UpdatedAt:   &Timestamp{referenceTime},
+		},
+		Installation: &Installation{
+			ID:       Int64(1),
+			NodeID:   String("nid"),
+			AppID:    Int64(1),
+			AppSlug:  String("as"),
+			TargetID: Int64(1),
+			Account: &User{
+				Login:           String("l"),
+				ID:              Int64(1),
+				URL:             String("u"),
+				AvatarURL:       String("a"),
+				GravatarID:      String("g"),
+				Name:            String("n"),
+				Company:         String("c"),
+				Blog:            String("b"),
+				Location:        String("l"),
+				Email:           String("e"),
+				Hireable:        Bool(true),
+				Bio:             String("b"),
+				TwitterUsername: String("t"),
+				PublicRepos:     Int(1),
+				Followers:       Int(1),
+				Following:       Int(1),
+				CreatedAt:       &Timestamp{referenceTime},
+				SuspendedAt:     &Timestamp{referenceTime},
+			},
+			AccessTokensURL:     String("atu"),
+			RepositoriesURL:     String("ru"),
+			HTMLURL:             String("hu"),
+			TargetType:          String("tt"),
+			SingleFileName:      String("sfn"),
+			RepositorySelection: String("rs"),
+			Events:              []string{"e"},
+			SingleFilePaths:     []string{"s"},
+			Permissions: &InstallationPermissions{
+				Actions:                       String("a"),
+				Administration:                String("ad"),
+				Checks:                        String("c"),
+				Contents:                      String("co"),
+				ContentReferences:             String("cr"),
+				Deployments:                   String("d"),
+				Environments:                  String("e"),
+				Issues:                        String("i"),
+				Metadata:                      String("md"),
+				Members:                       String("m"),
+				OrganizationAdministration:    String("oa"),
+				OrganizationHooks:             String("oh"),
+				OrganizationPlan:              String("op"),
+				OrganizationPreReceiveHooks:   String("opr"),
+				OrganizationProjects:          String("op"),
+				OrganizationSecrets:           String("os"),
+				OrganizationSelfHostedRunners: String("osh"),
+				OrganizationUserBlocking:      String("oub"),
+				Packages:                      String("pkg"),
+				Pages:                         String("pg"),
+				PullRequests:                  String("pr"),
+				RepositoryHooks:               String("rh"),
+				RepositoryProjects:            String("rp"),
+				RepositoryPreReceiveHooks:     String("rprh"),
+				Secrets:                       String("s"),
+				SecretScanningAlerts:          String("ssa"),
+				SecurityEvents:                String("se"),
+				SingleFile:                    String("sf"),
+				Statuses:                      String("s"),
+				TeamDiscussions:               String("td"),
+				VulnerabilityAlerts:           String("va"),
+				Workflows:                     String("w"),
+			},
+			CreatedAt:              &Timestamp{referenceTime},
+			UpdatedAt:              &Timestamp{referenceTime},
+			HasMultipleSingleFiles: Bool(false),
+			SuspendedBy: &User{
+				Login:           String("l"),
+				ID:              Int64(1),
+				URL:             String("u"),
+				AvatarURL:       String("a"),
+				GravatarID:      String("g"),
+				Name:            String("n"),
+				Company:         String("c"),
+				Blog:            String("b"),
+				Location:        String("l"),
+				Email:           String("e"),
+				Hireable:        Bool(true),
+				Bio:             String("b"),
+				TwitterUsername: String("t"),
+				PublicRepos:     Int(1),
+				Followers:       Int(1),
+				Following:       Int(1),
+				CreatedAt:       &Timestamp{referenceTime},
+				SuspendedAt:     &Timestamp{referenceTime},
+			},
+			SuspendedAt: &Timestamp{referenceTime},
+		},
+		Organization: &Organization{
+			BillingEmail:                         String("be"),
+			Blog:                                 String("b"),
+			Company:                              String("c"),
+			Email:                                String("e"),
+			TwitterUsername:                      String("tu"),
+			Location:                             String("loc"),
+			Name:                                 String("n"),
+			Description:                          String("d"),
+			IsVerified:                           Bool(true),
+			HasOrganizationProjects:              Bool(true),
+			HasRepositoryProjects:                Bool(true),
+			DefaultRepoPermission:                String("drp"),
+			MembersCanCreateRepos:                Bool(true),
+			MembersCanCreateInternalRepos:        Bool(true),
+			MembersCanCreatePrivateRepos:         Bool(true),
+			MembersCanCreatePublicRepos:          Bool(false),
+			MembersAllowedRepositoryCreationType: String("marct"),
+			MembersCanCreatePages:                Bool(true),
+			MembersCanCreatePublicPages:          Bool(false),
+			MembersCanCreatePrivatePages:         Bool(true),
+		},
+		Repository: &Repository{
+			ID:   Int64(1),
+			URL:  String("s"),
+			Name: String("n"),
+		},
+		Sender: &User{
+			Login:     String("l"),
+			ID:        Int64(1),
+			NodeID:    String("n"),
+			URL:       String("u"),
+			ReposURL:  String("r"),
+			EventsURL: String("e"),
+			AvatarURL: String("a"),
+		},
+	}
+
+	want := `{
+		"changes": {
+			"from": {
+				"security_and_analysis": {
+					"advanced_security": {
+						"status": "enabled"
+					},
+					"secret_scanning": {
+						"status": "enabled"
+					},
+					"secret_scanning_push_protection": {
+						"status": "enabled"
+					},
+					"dependabot_security_updates": {
+						"status": "enabled"
+					}
+				}
+			}
+		},
+		"enterprise": {
+			"id": 1,
+			"slug": "s",
+			"name": "n",
+			"node_id": "nid",
+			"avatar_url": "au",
+			"description": "d",
+			"website_url": "wu",
+			"html_url": "hu",
+			"created_at": ` + referenceTimeStr + `,
+			"updated_at": ` + referenceTimeStr + `
+		},
+		"installation": {
+			"id": 1,
+			"node_id": "nid",
+			"app_id": 1,
+			"app_slug": "as",
+			"target_id": 1,
+			"account": {
+				"login": "l",
+				"id": 1,
+				"avatar_url": "a",
+				"gravatar_id": "g",
+				"name": "n",
+				"company": "c",
+				"blog": "b",
+				"location": "l",
+				"email": "e",
+				"hireable": true,
+				"bio": "b",
+				"twitter_username": "t",
+				"public_repos": 1,
+				"followers": 1,
+				"following": 1,
+				"created_at": ` + referenceTimeStr + `,
+				"suspended_at": ` + referenceTimeStr + `,
+				"url": "u"
+			},
+			"access_tokens_url": "atu",
+			"repositories_url": "ru",
+			"html_url": "hu",
+			"target_type": "tt",
+			"single_file_name": "sfn",
+			"repository_selection": "rs",
+			"events": [
+				"e"
+			],
+			"single_file_paths": [
+				"s"
+			],
+			"permissions": {
+				"actions": "a",
+				"administration": "ad",
+				"checks": "c",
+				"contents": "co",
+				"content_references": "cr",
+				"deployments": "d",
+				"environments": "e",
+				"issues": "i",
+				"metadata": "md",
+				"members": "m",
+				"organization_administration": "oa",
+				"organization_hooks": "oh",
+				"organization_plan": "op",
+				"organization_pre_receive_hooks": "opr",
+				"organization_projects": "op",
+				"organization_secrets": "os",
+				"organization_self_hosted_runners": "osh",
+				"organization_user_blocking": "oub",
+				"packages": "pkg",
+				"pages": "pg",
+				"pull_requests": "pr",
+				"repository_hooks": "rh",
+				"repository_projects": "rp",
+				"repository_pre_receive_hooks": "rprh",
+				"secrets": "s",
+				"secret_scanning_alerts": "ssa",
+				"security_events": "se",
+				"single_file": "sf",
+				"statuses": "s",
+				"team_discussions": "td",
+				"vulnerability_alerts": "va",
+				"workflows": "w"
+			},
+			"created_at": ` + referenceTimeStr + `,
+			"updated_at": ` + referenceTimeStr + `,
+			"has_multiple_single_files": false,
+			"suspended_by": {
+				"login": "l",
+				"id": 1,
+				"avatar_url": "a",
+				"gravatar_id": "g",
+				"name": "n",
+				"company": "c",
+				"blog": "b",
+				"location": "l",
+				"email": "e",
+				"hireable": true,
+				"bio": "b",
+				"twitter_username": "t",
+				"public_repos": 1,
+				"followers": 1,
+				"following": 1,
+				"created_at": ` + referenceTimeStr + `,
+				"suspended_at": ` + referenceTimeStr + `,
+				"url": "u"
+			},
+			"suspended_at": ` + referenceTimeStr + `
+		},
+		"organization": {
+			"name": "n",
+			"company": "c",
+			"blog": "b",
+			"location": "loc",
+			"email": "e",
+			"twitter_username": "tu",
+			"description": "d",
+			"billing_email": "be",
+			"is_verified": true,
+			"has_organization_projects": true,
+			"has_repository_projects": true,
+			"default_repository_permission": "drp",
+			"members_can_create_repositories": true,
+			"members_can_create_public_repositories": false,
+			"members_can_create_private_repositories": true,
+			"members_can_create_internal_repositories": true,
+			"members_allowed_repository_creation_type": "marct",
+			"members_can_create_pages": true,
+			"members_can_create_public_pages": false,
+			"members_can_create_private_pages": true
+		},
+		"repository": {
+			"id": 1,
+			"url": "s",
+			"name": "n"
+		},
+		"sender": {
+			"login": "l",
+			"id": 1,
+			"node_id": "n",
+			"avatar_url": "a",
+			"url": "u",
+			"events_url": "e",
+			"repos_url": "r"
+		},
+		"target_type": "running"
+	}`
+
+	testJSONMarshal(t, u, want)
+}
+
 func TestCodeScanningAlertEvent_Marshal(t *testing.T) {
 	testJSONMarshal(t, &CodeScanningAlertEvent{}, "{}")
 
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index 712d8fc9..479f3c8d 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -23643,6 +23643,62 @@ func TestSecurityAndAnalysis_GetSecretScanningPushProtection(tt *testing.T) {
 	s.GetSecretScanningPushProtection()
 }
 
+func TestSecurityAndAnalysisChange_GetFrom(tt *testing.T) {
+	s := &SecurityAndAnalysisChange{}
+	s.GetFrom()
+	s = nil
+	s.GetFrom()
+}
+
+func TestSecurityAndAnalysisChangeFrom_GetSecurityAndAnalysis(tt *testing.T) {
+	s := &SecurityAndAnalysisChangeFrom{}
+	s.GetSecurityAndAnalysis()
+	s = nil
+	s.GetSecurityAndAnalysis()
+}
+
+func TestSecurityAndAnalysisEvent_GetChanges(tt *testing.T) {
+	s := &SecurityAndAnalysisEvent{}
+	s.GetChanges()
+	s = nil
+	s.GetChanges()
+}
+
+func TestSecurityAndAnalysisEvent_GetEnterprise(tt *testing.T) {
+	s := &SecurityAndAnalysisEvent{}
+	s.GetEnterprise()
+	s = nil
+	s.GetEnterprise()
+}
+
+func TestSecurityAndAnalysisEvent_GetInstallation(tt *testing.T) {
+	s := &SecurityAndAnalysisEvent{}
+	s.GetInstallation()
+	s = nil
+	s.GetInstallation()
+}
+
+func TestSecurityAndAnalysisEvent_GetOrganization(tt *testing.T) {
+	s := &SecurityAndAnalysisEvent{}
+	s.GetOrganization()
+	s = nil
+	s.GetOrganization()
+}
+
+func TestSecurityAndAnalysisEvent_GetRepository(tt *testing.T) {
+	s := &SecurityAndAnalysisEvent{}
+	s.GetRepository()
+	s = nil
+	s.GetRepository()
+}
+
+func TestSecurityAndAnalysisEvent_GetSender(tt *testing.T) {
+	s := &SecurityAndAnalysisEvent{}
+	s.GetSender()
+	s = nil
+	s.GetSender()
+}
+
 func TestSelectedReposList_GetTotalCount(tt *testing.T) {
 	var zeroValue int
 	s := &SelectedReposList{TotalCount: &zeroValue}
diff --git a/github/messages_test.go b/github/messages_test.go
index 8ee38eb3..b3828608 100644
--- a/github/messages_test.go
+++ b/github/messages_test.go
@@ -452,6 +452,10 @@ func TestParseWebHook(t *testing.T) {
 			payload:     &SecurityAdvisoryEvent{},
 			messageType: "security_advisory",
 		},
+		{
+			payload:     &SecurityAndAnalysisEvent{},
+			messageType: "security_and_analysis",
+		},
 		{
 			payload:     &StarEvent{},
 			messageType: "star",
diff --git a/github/repos_hooks_deliveries_test.go b/github/repos_hooks_deliveries_test.go
index d1dda365..a605c7f3 100644
--- a/github/repos_hooks_deliveries_test.go
+++ b/github/repos_hooks_deliveries_test.go
@@ -190,6 +190,7 @@ var hookDeliveryPayloadTypeToStruct = map[string]interface{}{
 	"repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{},
 	"secret_scanning_alert":          &SecretScanningAlertEvent{},
 	"security_advisory":              &SecurityAdvisoryEvent{},
+	"security_and_analysis":          &SecurityAndAnalysisEvent{},
 	"star":                           &StarEvent{},
 	"status":                         &StatusEvent{},
 	"team":                           &TeamEvent{},