Skip to content

Instantly share code, notes, and snippets.

@salrashid123
Created October 23, 2024 09:27
Show Gist options
  • Save salrashid123/58125749a795081f155fd1891751c929 to your computer and use it in GitHub Desktop.
Save salrashid123/58125749a795081f155fd1891751c929 to your computer and use it in GitHub Desktop.
slowly iterate GCP service accounts in an org for last authentication time
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