diff --git a/backend/controllers/orgs.go b/backend/controllers/orgs.go index 8bbfe3a71..dd586e8b8 100644 --- a/backend/controllers/orgs.go +++ b/backend/controllers/orgs.go @@ -39,10 +39,11 @@ func GetOrgSettingsApi(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{ - "drift_enabled": org.DriftEnabled, - "drift_cron_tab": org.DriftCronTab, - "drift_webhook_url": org.DriftWebhookUrl, - "billing_plan": org.BillingPlan, + "drift_enabled": org.DriftEnabled, + "drift_cron_tab": org.DriftCronTab, + "drift_webhook_url": org.DriftWebhookUrl, + "drift_teams_webhook_url": org.DriftTeamsWebhookUrl, + "billing_plan": org.BillingPlan, }) } @@ -66,6 +67,7 @@ func UpdateOrgSettingsApi(c *gin.Context) { DriftEnabled *bool `json:"drift_enabled,omitempty"` DriftCronTab *string `json:"drift_cron_tab,omitempty"` DriftWebhookUrl *string `json:"drift_webhook_url,omitempty"` + DriftTeamsWebhookUrl *string `json:"drift_teams_webhook_url,omitempty"` BillingPlan *string `json:"billing_plan,omitempty"` BillingStripeSubscriptionId *string `json:"billing_stripe_subscription_id,omitempty"` SlackChannelName *string `json:"slack_channel_name,omitempty"` @@ -89,6 +91,10 @@ func UpdateOrgSettingsApi(c *gin.Context) { org.DriftWebhookUrl = *reqBody.DriftWebhookUrl } + if reqBody.DriftTeamsWebhookUrl != nil { + org.DriftTeamsWebhookUrl = *reqBody.DriftTeamsWebhookUrl + } + if reqBody.BillingPlan != nil { org.BillingPlan = models.BillingPlan(*reqBody.BillingPlan) } diff --git a/backend/models/orgs.go b/backend/models/orgs.go index 0b98bb2c1..91dad1b95 100644 --- a/backend/models/orgs.go +++ b/backend/models/orgs.go @@ -23,6 +23,7 @@ type Organisation struct { ExternalId string `gorm:"uniqueIndex:idx_external_source"` DriftEnabled bool `gorm:"default:false"` DriftWebhookUrl string + DriftTeamsWebhookUrl string DriftCronTab string `gorm:"default:'0 0 * * *'"` BillingPlan BillingPlan `gorm:"default:'free'"` BillingStripeSubscriptionId string diff --git a/ee/drift/controllers/notifications.go b/ee/drift/controllers/notifications.go index 6cb892a36..3b383f8f6 100644 --- a/ee/drift/controllers/notifications.go +++ b/ee/drift/controllers/notifications.go @@ -78,10 +78,73 @@ func sendTestSlackWebhook(webhookURL string) error { return nil } +func sendTestTeamsWebhook(webhookURL string) error { + messageCard := map[string]interface{}{ + "@type": "MessageCard", + "@context": "http://schema.org/extensions", + "themeColor": "0076D7", + "summary": "Digger Drift Detection Test", + "sections": []map[string]interface{}{ + { + "activityTitle": "Digger Drift Detection", + "activitySubtitle": "Test Notification", + "activityText": "This is a test notification to verify your MS Teams integration is working correctly.", + "facts": []map[string]string{ + {"name": "Project", "value": "Dev environment"}, + {"name": "Status", "value": "🟡 Drift detected"}, + }, + }, + { + "activityTitle": "Environment Status", + "activitySubtitle": "Current Status Overview", + "facts": []map[string]string{ + {"name": "Dev environment", "value": "🟡 Drift detected"}, + {"name": "Staging environment", "value": "⚪ Acknowledged drift"}, + {"name": "Prod environment", "value": "🟢 No drift"}, + }, + }, + { + "activityTitle": "Note", + "activityText": "✅ This is a test notification", + }, + }, + "potentialAction": []map[string]interface{}{ + { + "@type": "OpenUri", + "name": "View Dashboard", + "targets": []map[string]interface{}{ + {"os": "default", "uri": os.Getenv("DIGGER_APP_URL")}, + }, + }, + }, + } + + jsonPayload, err := json.Marshal(messageCard) + if err != nil { + return fmt.Errorf("error marshalling JSON: %v", err) + } + + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonPayload)) + if err != nil { + return fmt.Errorf("error sending POST request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non-OK HTTP status: %s", resp.Status) + } + + return nil +} + type TestSlackNotificationForUrl struct { SlackNotificationUrl string `json:"notification_url"` } +type TestTeamsNotificationForUrl struct { + TeamsNotificationUrl string `json:"notification_url"` +} + func (mc MainController) SendTestSlackNotificationForUrl(c *gin.Context) { var request TestSlackNotificationForUrl err := c.BindJSON(&request) @@ -102,6 +165,26 @@ func (mc MainController) SendTestSlackNotificationForUrl(c *gin.Context) { c.String(200, "ok") } +func (mc MainController) SendTestTeamsNotificationForUrl(c *gin.Context) { + var request TestTeamsNotificationForUrl + err := c.BindJSON(&request) + if err != nil { + log.Printf("Error binding JSON: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error binding JSON"}) + return + } + teamsNotificationUrl := request.TeamsNotificationUrl + + err = sendTestTeamsWebhook(teamsNotificationUrl) + if err != nil { + log.Printf("Error sending teams notification: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error sending teams notification"}) + return + } + + c.String(200, "ok") +} + func sectionBlockForProject(project models.Project) (*slack.SectionBlock, error) { switch project.DriftStatus { case models.DriftStatusNoDrift: @@ -217,6 +300,128 @@ func (mc MainController) SendRealSlackNotificationForOrg(c *gin.Context) { c.String(200, "ok") } +func createTeamsMessageCardForProjects(projects []*models.Project) map[string]interface{} { + facts := []map[string]string{ + {"name": "Project", "value": "Status"}, + } + + var sections []map[string]interface{} + + for _, project := range projects { + if project.DriftEnabled { + var statusValue string + + switch project.DriftStatus { + case models.DriftStatusNoDrift: + statusValue = "🟢 No Drift" + case models.DriftStatusAcknowledgeDrift: + statusValue = "⚪ Acknowledged Drift" + case models.DriftStatusNewDrift: + statusValue = "🟡 Drift detected" + default: + statusValue = "❓ Unknown" + } + + facts = append(facts, map[string]string{ + "name": project.Name, + "value": statusValue, + }) + } + } + + section := map[string]interface{}{ + "activityTitle": "Digger Drift Detection Report", + "activitySubtitle": fmt.Sprintf("Found %d projects with drift enabled", len(facts)-1), + "facts": facts, + } + + sections = append(sections, section) + + messageCard := map[string]interface{}{ + "@type": "MessageCard", + "@context": "http://schema.org/extensions", + "themeColor": "0076D7", + "summary": "Digger Drift Detection Report", + "sections": sections, + "potentialAction": []map[string]interface{}{ + { + "@type": "OpenUri", + "name": "View Dashboard", + "targets": []map[string]interface{}{ + {"os": "default", "uri": os.Getenv("DIGGER_APP_URL")}, + }, + }, + }, + } + + return messageCard +} + +func (mc MainController) SendRealTeamsNotificationForOrg(c *gin.Context) { + var request RealSlackNotificationForOrgRequest + err := c.BindJSON(&request) + if err != nil { + log.Printf("Error binding JSON: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error binding JSON"}) + return + } + orgId := request.OrgId + + org, err := models.DB.GetOrganisationById(orgId) + if err != nil { + log.Printf("could not get org %v err: %v", orgId, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not get org %v", orgId)}) + return + } + + teamsNotificationUrl := org.DriftTeamsWebhookUrl + + projects, err := models.DB.LoadProjectsForOrg(orgId) + if err != nil { + log.Printf("could not load projects for org %v err: %v", orgId, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not load projects for org %v", orgId)}) + return + } + + numOfProjectsWithDriftEnabled := 0 + for _, project := range projects { + if project.DriftEnabled { + numOfProjectsWithDriftEnabled++ + } + } + + if numOfProjectsWithDriftEnabled == 0 { + log.Printf("no projects with drift enabled for org: %v, succeeding", orgId) + c.String(200, "ok") + return + } + + messageCard := createTeamsMessageCardForProjects(projects) + + jsonPayload, err := json.Marshal(messageCard) + if err != nil { + log.Printf("error marshalling teams message card: %v", err) + c.JSON(500, gin.H{"error": "error marshalling teams message card"}) + return + } + + resp, err := http.Post(teamsNotificationUrl, "application/json", bytes.NewBuffer(jsonPayload)) + if err != nil { + log.Printf("error sending teams webhook: %v", err) + c.JSON(500, gin.H{"error": "error sending teams webhook"}) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Printf("teams webhook got unexpected status for org: %v - status: %v", org.ID, resp.StatusCode) + c.JSON(500, gin.H{"error": "teams webhook got unexpected status"}) + return + } + + c.String(200, "ok") +} + func (mc MainController) ProcessAllNotifications(c *gin.Context) { diggerHostname := os.Getenv("DIGGER_HOSTNAME") webhookSecret := os.Getenv("DIGGER_WEBHOOK_SECRET") @@ -233,6 +438,13 @@ func (mc MainController) ProcessAllNotifications(c *gin.Context) { return } + sendTeamsNotificationUrl, err := url.JoinPath(diggerHostname, "_internal/send_teams_notification_for_org") + if err != nil { + log.Printf("could not form teams drift url: %v", err) + c.JSON(500, gin.H{"error": "could not form teams drift url"}) + return + } + for _, org := range orgs { if org.DriftEnabled == false { continue @@ -280,6 +492,45 @@ func (mc MainController) ProcessAllNotifications(c *gin.Context) { if statusCode != 200 { log.Printf("send slack notification got unexpected status for org: %v - status: %v", org.ID, statusCode) } + + // Send MS Teams notification if webhook URL is configured + if org.DriftTeamsWebhookUrl != "" { + fmt.Println("Sending teams notification for org ID: ", org.ID) + teamsPayload := RealSlackNotificationForOrgRequest{OrgId: org.ID} + + // Convert payload to JSON + teamsJsonPayload, err := json.Marshal(teamsPayload) + if err != nil { + fmt.Println("Process Teams notification: error marshaling JSON:", err) + continue + } + + // Create a new request for MS Teams + teamsReq, err := http.NewRequest("POST", sendTeamsNotificationUrl, bytes.NewBuffer(teamsJsonPayload)) + if err != nil { + fmt.Println("Process teams notification: Error creating request:", err) + continue + } + + // Set headers + teamsReq.Header.Set("Content-Type", "application/json") + teamsReq.Header.Set("Authorization", fmt.Sprintf("Bearer %v", webhookSecret)) + + // Send the request + teamsClient := &http.Client{} + teamsResp, err := teamsClient.Do(teamsReq) + if err != nil { + fmt.Println("Error sending teams request:", err) + continue + } + teamsResp.Body.Close() + + // Get the status code + teamsStatusCode := teamsResp.StatusCode + if teamsStatusCode != 200 { + log.Printf("send teams notification got unexpected status for org: %v - status: %v", org.ID, teamsStatusCode) + } + } } } diff --git a/ee/drift/main.go b/ee/drift/main.go index be98d4903..a61092b51 100644 --- a/ee/drift/main.go +++ b/ee/drift/main.go @@ -92,6 +92,8 @@ func main() { r.POST("/_internal/process_notifications", middleware.WebhookAuth(), controller.ProcessAllNotifications) r.POST("/_internal/send_slack_notification_for_org", middleware.WebhookAuth(), controller.SendRealSlackNotificationForOrg) r.POST("/_internal/send_test_slack_notification_for_url", middleware.WebhookAuth(), controller.SendTestSlackNotificationForUrl) + r.POST("/_internal/send_teams_notification_for_org", middleware.WebhookAuth(), controller.SendRealTeamsNotificationForOrg) + r.POST("/_internal/send_test_teams_notification_for_url", middleware.WebhookAuth(), controller.SendTestTeamsNotificationForUrl) r.POST("/_internal/process_drift", middleware.WebhookAuth(), controller.ProcessAllDrift) r.POST("/_internal/process_drift_for_org", middleware.WebhookAuth(), controller.ProcessDriftForOrg) diff --git a/go.work.sum b/go.work.sum index 1e3b7a1a4..c6c4486dc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -947,6 +947,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= @@ -1100,6 +1101,7 @@ github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMc github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/cloud-bigtable-clients-test v0.0.2 h1:S+sCHWAiAc+urcEnvg5JYJUOdlQEm/SEzQ/c/IdAH5M= github.com/googleapis/cloud-bigtable-clients-test v0.0.2/go.mod h1:mk3CrkrouRgtnhID6UZQDK3DrFFa7cYCAJcEmNsHYrY= github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= @@ -1462,6 +1464,7 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=