Skip to content

Commit bd8f8ad

Browse files
feat: support deletion of Cloud Asset Inventory feeds not in use in organization (#198)
Co-authored-by: Daniel Andrade <dandrade@ciandt.com>
1 parent 6695683 commit bd8f8ad

File tree

6 files changed

+129
-5
lines changed

6 files changed

+129
-5
lines changed

‎modules/project_cleanup/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ The following services must be enabled on the project housing the cleanup functi
1616
- Cloud Scheduler (`cloudscheduler.googleapis.com`)
1717
- Cloud Resource Manager (`cloudresourcemanager.googleapis.com`)
1818
- Compute Engine API (`compute.googleapis.com`)
19+
- Cloud Asset API (`cloudasset.googleapis.com`)
1920
- Security Command Center API (`securitycenter.googleapis.com`)
2021

2122
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
2223
## Inputs
2324

2425
| Name | Description | Type | Default | Required |
2526
|------|-------------|------|---------|:--------:|
27+
| clean\_up\_org\_level\_cai\_feeds | Clean up organization level Cloud Asset Inventory Feeds. | `bool` | `false` | no |
2628
| clean\_up\_org\_level\_scc\_notifications | Clean up organization level Security Command Center notifications. | `bool` | `false` | no |
2729
| clean\_up\_org\_level\_tag\_keys | Clean up organization level Tag Keys. | `bool` | `false` | no |
2830
| function\_timeout\_s | The amount of time in seconds allotted for the execution of the function. | `number` | `500` | no |
@@ -35,6 +37,7 @@ The following services must be enabled on the project housing the cleanup functi
3537
| target\_excluded\_labels | Map of project lablels that won't be deleted. | `map(string)` | `{}` | no |
3638
| target\_excluded\_tagkeys | List of organization Tag Key short names that won't be deleted. | `list(string)` | `[]` | no |
3739
| target\_folder\_id | Folder ID to delete all projects under. | `string` | `""` | no |
40+
| target\_included\_feeds | List of organization level Cloud Asset Inventory feeds that should be deleted. Regex example: `.*/feeds/fd-cai-monitoring-.*` | `list(string)` | `[]` | no |
3841
| target\_included\_labels | Map of project lablels that will be deleted. | `map(string)` | `{}` | no |
3942
| target\_included\_scc\_notifications | List of organization Security Command Center notifications names regex that will be deleted. Regex example: `.*/notificationConfigs/scc-notify-.*` | `list(string)` | `[]` | no |
4043
| target\_tag\_name | The name of a tag to filter GCP projects on for consideration by the cleanup utility (legacy, use `target_included_labels` map instead). | `string` | `""` | no |

‎modules/project_cleanup/function_source/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/terraform-google-modules/terraform-google-scheduled-function/m
33
go 1.21
44

55
require (
6+
cloud.google.com/go/asset v1.17.2
67
cloud.google.com/go/securitycenter v1.29.0
78
golang.org/x/net v0.24.0
89
golang.org/x/oauth2 v0.20.0
@@ -11,11 +12,14 @@ require (
1112

1213
require (
1314
cloud.google.com/go v0.112.2 // indirect
15+
cloud.google.com/go/accesscontextmanager v1.8.5 // indirect
1416
cloud.google.com/go/auth v0.3.0 // indirect
1517
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
1618
cloud.google.com/go/compute/metadata v0.3.0 // indirect
1719
cloud.google.com/go/iam v1.1.7 // indirect
1820
cloud.google.com/go/longrunning v0.5.6 // indirect
21+
cloud.google.com/go/orgpolicy v1.12.1 // indirect
22+
cloud.google.com/go/osconfig v1.12.5 // indirect
1923
github.com/felixge/httpsnoop v1.0.4 // indirect
2024
github.com/go-logr/logr v1.4.1 // indirect
2125
github.com/go-logr/stdr v1.2.2 // indirect

‎modules/project_cleanup/function_source/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
22
cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw=
33
cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
4+
cloud.google.com/go/accesscontextmanager v1.8.5 h1:2GLNaNu9KRJhJBFTIVRoPwk6xE5mUDgD47abBq4Zp/I=
5+
cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q=
6+
cloud.google.com/go/asset v1.17.2 h1:xgFnBP3luSbUcC9RWJvb3Zkt+y/wW6PKwPHr3ssnIP8=
7+
cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4=
48
cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs=
59
cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
610
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
@@ -11,6 +15,10 @@ cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
1115
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
1216
cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE=
1317
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
18+
cloud.google.com/go/orgpolicy v1.12.1 h1:2JbXigqBJVp8Dx5dONUttFqewu4fP0p3pgOdIZAhpYU=
19+
cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I=
20+
cloud.google.com/go/osconfig v1.12.5 h1:Mo5jGAxOMKH/PmDY7fgY19yFcVbvwREb5D5zMPQjFfo=
21+
cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8=
1422
cloud.google.com/go/securitycenter v1.29.0 h1:jKqAqF4iLwAzCe9vzncKuxGKStmxX/HL0ItvQJdFqEA=
1523
cloud.google.com/go/securitycenter v1.29.0/go.mod h1:1+5P3FIDLvV2lQ83UFn+aQRb5emi4ew2XYrMmd1GSHU=
1624
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

‎modules/project_cleanup/function_source/main.go

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
"strings"
2828
"time"
2929

30+
asset "cloud.google.com/go/asset/apiv1"
31+
"cloud.google.com/go/asset/apiv1/assetpb"
3032
securitycenter "cloud.google.com/go/securitycenter/apiv1"
3133
"cloud.google.com/go/securitycenter/apiv1/securitycenterpb"
3234
"golang.org/x/net/context"
@@ -55,6 +57,8 @@ const (
5557
targetFolderRegexp = `^[0-9]+$`
5658
targetOrganizationRegexp = `^[0-9]+$`
5759
SCCNotificationsPageSize = "SCC_NOTIFICATIONS_PAGE_SIZE"
60+
CleanUpCaiFeeds = "CLEAN_UP_CAI_FEEDS"
61+
TargetIncludedFeeds = "TARGET_INCLUDED_FEEDS"
5862
)
5963

6064
var (
@@ -69,6 +73,8 @@ var (
6973
rootFolderId = getCorrectFolderIdOrTerminateExecution()
7074
organizationId = getCorrectOrganizationIdOrTerminateExecution()
7175
sccPageSize = getSCCNotificationPageSizeOrTerminateExecution()
76+
cleanUpCaiFeeds = getCleanUpFeedsOrTerminateExecution()
77+
includedFeedsList = getFeedsListFromEnv(TargetIncludedFeeds)
7278
)
7379

7480
type PubSubMessage struct {
@@ -172,12 +178,12 @@ func checkIfAtLeastOneLabelPresentIfAny(project *cloudresourcemanager.Project, l
172178
return result
173179
}
174180

175-
func checkIfSCCNotificationNameIncluded(notificationName string, includedSCCNotfis []*regexp.Regexp) bool {
176-
if len(includedSCCNotfis) == 0 {
181+
func checkIfNameIncluded(name string, reg []*regexp.Regexp) bool {
182+
if len(reg) == 0 {
177183
return false
178184
}
179-
for _, name := range includedSCCNotfis {
180-
if name.MatchString(notificationName) {
185+
for _, regex := range reg {
186+
if regex.MatchString(name) {
181187
return true
182188
}
183189
}
@@ -296,6 +302,48 @@ func getSCCNotificationPageSizeOrTerminateExecution() int32 {
296302
return int32(size)
297303
}
298304

305+
func getFeedsListFromEnv(envVariableName string) []*regexp.Regexp {
306+
var compiledRegEx []*regexp.Regexp
307+
targetIncludedFeeds := os.Getenv(envVariableName)
308+
logger.Println("Try to get CAI Feeds list")
309+
if targetIncludedFeeds == "" {
310+
logger.Printf("No CAI Feeds provided.")
311+
return compiledRegEx
312+
}
313+
314+
var caiFeeds []string
315+
err := json.Unmarshal([]byte(targetIncludedFeeds), &caiFeeds)
316+
if err != nil {
317+
logger.Printf("Failed to get CAI Feeds list from [%s] env variable, error [%s]", envVariableName, err.Error())
318+
return nil
319+
} else {
320+
logger.Printf("Got CAI Feeds list [%s] from [%s] env variable", caiFeeds, envVariableName)
321+
}
322+
323+
//build Regexes
324+
for _, r := range caiFeeds {
325+
result, err := regexp.Compile(r)
326+
if err != nil {
327+
logger.Printf("Invalid regular expression [%s] for CAI Feed", r)
328+
} else {
329+
compiledRegEx = append(compiledRegEx, result)
330+
}
331+
}
332+
return compiledRegEx
333+
}
334+
335+
func getCleanUpFeedsOrTerminateExecution() bool {
336+
cleanUpCaiFeeds, exists := os.LookupEnv(CleanUpCaiFeeds)
337+
if !exists {
338+
logger.Fatalf("Clean up CAI Feeds environment variable [%s] not set, set the environment variable and try again.", CleanUpCaiFeeds)
339+
}
340+
result, err := strconv.ParseBool(cleanUpCaiFeeds)
341+
if err != nil {
342+
logger.Fatalf("Invalid Clean up CAI Feeds value [%s], specify correct value for environment variable [%s] and try again.", cleanUpCaiFeeds, CleanUpCaiFeeds)
343+
}
344+
return result
345+
}
346+
299347
func getCorrectFolderIdOrTerminateExecution() string {
300348
targetFolderIdString := os.Getenv(TargetFolderId)
301349
matched, err := regexp.MatchString(targetFolderRegexp, targetFolderIdString)
@@ -372,6 +420,16 @@ func getSCCNotificationServiceOrTerminateExecution(ctx context.Context, client *
372420
return securitycenterClient
373421
}
374422

423+
func getAssetServiceOrTerminateExecution(ctx context.Context, client *http.Client) *asset.Client {
424+
logger.Println("Try to get Asset Service")
425+
assetService, err := asset.NewClient(ctx)
426+
if err != nil {
427+
logger.Fatalf("Failed to get Asset Service with error [%s], terminate execution", err.Error())
428+
}
429+
logger.Println("Got Asset Service")
430+
return assetService
431+
}
432+
375433
func getFirewallPoliciesServiceOrTerminateExecution(ctx context.Context, client *http.Client) *compute.FirewallPoliciesService {
376434
logger.Println("Try to get Firewall Policies Service")
377435
computeService, err := compute.NewService(ctx, option.WithHTTPClient(client))
@@ -399,6 +457,7 @@ func invoke(ctx context.Context) {
399457
tagKeyService := getTagKeysServiceOrTerminateExecution(ctx, client)
400458
sccService := getSCCNotificationServiceOrTerminateExecution(ctx, client)
401459
tagValuesService := getTagValuesServiceOrTerminateExecution(ctx, client)
460+
feedsService := getAssetServiceOrTerminateExecution(ctx, client)
402461
firewallPoliciesService := getFirewallPoliciesServiceOrTerminateExecution(ctx, client)
403462
endpointService := getServiceManagementServiceOrTerminateExecution(ctx, client)
404463

@@ -450,7 +509,7 @@ func invoke(ctx context.Context) {
450509
break
451510
}
452511
projectID := strings.Split(resp.PubsubTopic, "/")[1]
453-
if checkIfSCCNotificationNameIncluded(resp.Name, includedSCCNotfisList) && projectDeleteRequestedFilter(projectID) {
512+
if checkIfNameIncluded(resp.Name, includedSCCNotfisList) && projectDeleteRequestedFilter(projectID) {
454513
delReq := &securitycenterpb.DeleteNotificationConfigRequest{
455514
Name: resp.Name,
456515
}
@@ -498,6 +557,35 @@ func invoke(ctx context.Context) {
498557
}
499558
}
500559

560+
removeFeedsByName := func(organization string) {
561+
logger.Printf("Try to remove feeds from organization [%s]", organization)
562+
563+
req := &assetpb.ListFeedsRequest{
564+
Parent: fmt.Sprintf("organizations/%s", organization),
565+
}
566+
567+
resp, err := feedsService.ListFeeds(ctx, req)
568+
if err != nil {
569+
logger.Printf("Failed to list Feeds, error [%s]", err.Error())
570+
return
571+
}
572+
573+
for _, feed := range resp.Feeds {
574+
projectID := strings.Split(feed.FeedOutputConfig.GetPubsubDestination().Topic, "/")[1]
575+
if checkIfNameIncluded(feed.Name, includedFeedsList) && projectDeleteRequestedFilter(projectID) {
576+
delReq := &assetpb.DeleteFeedRequest{
577+
Name: feed.Name,
578+
}
579+
err := feedsService.DeleteFeed(ctx, delReq)
580+
if err != nil {
581+
logger.Printf("Failed to remove the feed [%s], error [%s]", feed.Name, err.Error())
582+
} else {
583+
logger.Printf("Feed [%s] successfully removed.", feed.Name)
584+
}
585+
}
586+
}
587+
}
588+
501589
removeFirewallPolicies := func(folder string) {
502590
logger.Printf("Try to remove Firewall Policies from folder [%s]", folder)
503591
firewallPolicyList, err := firewallPoliciesService.List().ParentId(folder).Context(ctx).Do()
@@ -640,6 +728,7 @@ func invoke(ctx context.Context) {
640728
} else {
641729
getSubFoldersAndRemoveProjectsFoldersRecursively(rootFolder, getSubFoldersAndRemoveProjectsFoldersRecursively)
642730
}
731+
643732
// Only Tag Keys whose values are not in use can be deleted.
644733
if cleanUpTagKeys {
645734
removeTagKeys(organizationId)
@@ -649,6 +738,11 @@ func invoke(ctx context.Context) {
649738
if cleanUpSCCNotfi {
650739
removeSCCNotifications(organizationId)
651740
}
741+
742+
//Only delete Feeds from deleted projects
743+
if cleanUpCaiFeeds {
744+
removeFeedsByName(organizationId)
745+
}
652746
}
653747

654748
func CleanUpProjects(ctx context.Context, m PubSubMessage) error {

‎modules/project_cleanup/main.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ resource "google_organization_iam_member" "main" {
3434
"roles/compute.orgSecurityPolicyAdmin",
3535
"roles/resourcemanager.tagAdmin",
3636
"roles/viewer",
37+
"roles/cloudasset.owner",
3738
"roles/securitycenter.notificationConfigEditor"
3839
])
3940

@@ -69,5 +70,7 @@ module "scheduled_project_cleaner" {
6970
CLEAN_UP_SCC_NOTIFICATIONS = var.clean_up_org_level_scc_notifications
7071
TARGET_INCLUDED_SCC_NOTIFICATIONS = jsonencode(var.target_included_scc_notifications)
7172
SCC_NOTIFICATIONS_PAGE_SIZE = var.list_scc_notifications_page_size
73+
CLEAN_UP_CAI_FEEDS = var.clean_up_org_level_cai_feeds
74+
TARGET_INCLUDED_FEEDS = jsonencode(var.target_included_feeds)
7275
}
7376
}

‎modules/project_cleanup/variables.tf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ variable "organization_id" {
2525
description = "The organization ID whose projects to clean up"
2626
}
2727

28+
variable "clean_up_org_level_cai_feeds" {
29+
type = bool
30+
description = "Clean up organization level Cloud Asset Inventory Feeds."
31+
default = false
32+
}
33+
34+
variable "target_included_feeds" {
35+
type = list(string)
36+
description = "List of organization level Cloud Asset Inventory feeds that should be deleted. Regex example: `.*/feeds/fd-cai-monitoring-.*` "
37+
default = []
38+
}
39+
2840
variable "project_id" {
2941
type = string
3042
description = "The project ID to host the scheduled function in"

0 commit comments

Comments
 (0)