Created
October 23, 2024 09:27
-
-
Save salrashid123/58125749a795081f155fd1891751c929 to your computer and use it in GitHub Desktop.
slowly iterate GCP service accounts in an org for last authentication time
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
// gcloud auth application-default login | |
// export USER=`gcloud config get-value core/account` | |
// export PROJECT_ID=`gcloud config get-value core/project` | |
// export QUOTA_PROJECT=$PROJECT_ID | |
// export ORGANIZATION_ID="organizations/1111111" | |
// gcloud services enable policyanalyzer.googleapis.com | |
// gcloud projects add-iam-policy-binding --role=roles/serviceusage.serviceUsageConsumer --member=user:$USER $QUOTA_PROJECT | |
// go run main.go --organization $ORGANIZATION_ID --quotaProject=$QUOTA_PROJECT --alsologtostderr=1 -v 30 | |
import ( | |
"encoding/json" | |
"flag" | |
"fmt" | |
"strings" | |
"time" | |
asset "cloud.google.com/go/asset/apiv1" | |
resourcemanager "cloud.google.com/go/resourcemanager/apiv3" | |
"cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" | |
"github.com/golang/glog" | |
"golang.org/x/net/context" | |
"golang.org/x/oauth2" | |
"golang.org/x/oauth2/google" | |
"google.golang.org/api/policyanalyzer/v1" | |
assetpb "cloud.google.com/go/asset/apiv1/assetpb" | |
"google.golang.org/api/impersonate" | |
"google.golang.org/api/iterator" | |
"google.golang.org/api/option" | |
) | |
const ( | |
assetTypeServiceAccountKey string = "iam.googleapis.com/ServiceAccountKey" | |
maxPageSize int64 = 1000 | |
burst int = 1 | |
maxRequestsPerSecond float64 = 1 // "golang.org/x/time/rate" limiter to throttle operations | |
iamServiceAccountsRegex = "//iam.googleapis.com/projects/(.+)/serviceAccounts/(.+)" | |
serviceAccountsKeysRegex = "//iam.googleapis.com/projects/(.+)/serviceAccounts/(.+)/keys/(.+)" | |
) | |
const ( | |
activityTypeAuthentication = "serviceAccountLastAuthentication" | |
activityTypeKeyAuthentication = "serviceAccountKeyLastAuthentication" | |
) | |
type serviceAccountLastAuthentication struct { | |
LastAuthenticatedTime string `json:"lastAuthenticatedTime"` | |
ServiceAccount struct { | |
FullResourceName string `json:"fullResourceName"` | |
ProjectNumber string `json:"projectNumber"` | |
ServiceAccountId string `json:"serviceAccountId"` | |
} `json:"serviceAccount,omitempty"` | |
ServiceAccountKey struct { | |
FullResourceName string `json:"fullResourceName"` | |
ProjectNumber string `json:"projectNumber"` | |
ServiceAccountId string `json:"serviceAccountId"` | |
} `json:"serviceAccountKey,omitempty"` | |
} | |
func main() { | |
impersonatedServiceAccount := flag.String("impersonatedServiceAccount", "", "Impersonated Service Accounts the script should run as") | |
quotaProject := flag.String("quotaProject", "", "The cloudasset.googleapis.com API requires a quota project, which is not set by default. To learn how to set your quota project, see https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds") | |
organization := flag.String("organization", "", "The organizationID that is the subject of this audit") | |
rotationDays := flag.Int("rotationDays", 90, "Number of days ago the key was created") | |
flag.Parse() | |
defer glog.Flush() | |
ctx := context.Background() | |
if !strings.HasPrefix(*organization, "organizations/") { | |
glog.Fatalln("--organizations= must be formatted as organizations/YOUR_ORG_ID") | |
} | |
if *quotaProject == "" { | |
glog.Fatalln("The cloudasset.googleapis.com API requires a quota project") | |
} | |
var ts oauth2.TokenSource | |
var err error | |
if *impersonatedServiceAccount != "" { | |
ts, err = impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ | |
TargetPrincipal: *impersonatedServiceAccount, | |
Scopes: resourcemanager.DefaultAuthScopes(), | |
}) | |
if err != nil { | |
glog.Fatalln(err) | |
} | |
} else { | |
ts, err = google.DefaultTokenSource(ctx, resourcemanager.DefaultAuthScopes()...) | |
if err != nil { | |
glog.Fatalln(err) | |
} | |
} | |
glog.V(20).Infoln(" Getting Projects") | |
c, err := resourcemanager.NewProjectsClient(ctx, option.WithTokenSource(ts), option.WithQuotaProject(*quotaProject)) | |
if err != nil { | |
glog.Fatalln(err) | |
} | |
projects := make([]*resourcemanagerpb.Project, 0) | |
req := &resourcemanagerpb.ListProjectsRequest{ | |
Parent: *organization, | |
// TODO: Fill request struct fields. | |
// See https://pkg.go.dev/cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb#ListProjectsRequest. | |
} | |
pit := c.ListProjects(ctx, req) | |
for { | |
p, err := pit.Next() | |
if err == iterator.Done { | |
break | |
} | |
if err != nil { | |
glog.Fatalln(err) | |
} | |
fmt.Printf("Project Name: %s\n", p.Name) | |
projects = append(projects, p) | |
} | |
glog.V(20).Infoln(" Getting ServiceAccounts") | |
assetClient, err := asset.NewClient(ctx, option.WithTokenSource(ts), option.WithQuotaProject(*quotaProject)) | |
if err != nil { | |
glog.Fatalln(err) | |
} | |
now := time.Now().AddDate(0, 0, -*rotationDays).UTC() | |
queryFilter := fmt.Sprintf("createTime < %s", now.Format("2006-01-02")) | |
allSvcAccounts, err := findResourcesByAssetType(ctx, *organization, assetTypeServiceAccountKey, queryFilter, "createTime", assetClient) | |
if err != nil { | |
glog.Fatalf("Error finding all projects in the organization %v", err) | |
} | |
glog.V(20).Infof("Service count: %v\n", len(allSvcAccounts)) | |
/// ************************** | |
delay := flag.Int("delay", 60, "delay in s") | |
policyanalyzerService, err := policyanalyzer.NewService(ctx) | |
if err != nil { | |
glog.Fatalf("%v", err) | |
} | |
for _, s := range projects { | |
//for _, s := range allSvcAccounts { | |
//re := regexp.MustCompile(iamServiceAccountsRegex) | |
//res := re.FindStringSubmatch(s.ParentFullResourceName) | |
//parent := fmt.Sprintf("projects/%s/locations/global/activityTypes/%s", res[1], activityTypeKeyAuthentication) | |
parent := fmt.Sprintf("projects/%s/locations/global/activityTypes/%s", s.ProjectId, activityTypeKeyAuthentication) | |
//filter := fmt.Sprintf("activities.full_resource_name=\"%s\"", s.Name) | |
filter := "" | |
err = policyanalyzerService.Projects.Locations.ActivityTypes.Activities.Query(parent).Filter(filter).Pages(ctx, func(g *policyanalyzer.GoogleCloudPolicyanalyzerV1QueryActivityResponse) error { | |
for _, m := range g.Activities { | |
b, err := m.Activity.MarshalJSON() | |
if err != nil { | |
return err | |
} | |
var sa serviceAccountLastAuthentication | |
err = json.Unmarshal(b, &sa) | |
if err != nil { | |
return err | |
} | |
if sa.LastAuthenticatedTime != "" { | |
glog.V(20).Infof("%s ObservationPeriod: (%s --> %s)\n", m.ActivityType, m.ObservationPeriod.StartTime, m.ObservationPeriod.EndTime) | |
glog.V(20).Infof(" Key: %s", m.FullResourceName) | |
glog.V(20).Infof(" ServiceAccountKey.LastAuthenticatedTime %s\n", sa.LastAuthenticatedTime) | |
} | |
} | |
return nil | |
}) | |
if err != nil { | |
glog.Fatalf("%v", err) | |
} | |
time.Sleep(time.Duration(*delay) * time.Second) | |
} | |
} | |
func findResourcesByAssetType(ctx context.Context, organizationID string, assetType string, query string, orderBy string, assetClient *asset.Client) (map[string]*assetpb.ResourceSearchResult, error) { | |
resourceList := make(map[string]*assetpb.ResourceSearchResult) | |
req := &assetpb.SearchAllResourcesRequest{ | |
Scope: organizationID, | |
Query: query, | |
AssetTypes: []string{assetType}, | |
PageSize: int32(maxPageSize), | |
OrderBy: orderBy, | |
} | |
it := assetClient.SearchAllResources(ctx, req) | |
for { | |
response, err := it.Next() | |
if err == iterator.Done { | |
break | |
} | |
if err != nil { | |
return nil, err | |
} | |
switch { | |
case assetType == assetTypeServiceAccountKey: | |
//glog.V(20).Infof(" Found ServiceAccount %s", response.Name) | |
resourceList[response.Name] = response | |
default: | |
return nil, fmt.Errorf("error getting resources: unknown assetType: %s", assetType) | |
} | |
} | |
return resourceList, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment