github.com/google/osv-scalibr@v0.4.1/enricher/hcpidentity/hcpidentity.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package hcpidentity contains an Enricher that augments HCP access tokens 16 // with identity metadata from the caller-identity endpoint. 17 package hcpidentity 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 26 "github.com/google/osv-scalibr/enricher" 27 "github.com/google/osv-scalibr/inventory" 28 "github.com/google/osv-scalibr/plugin" 29 "github.com/google/osv-scalibr/veles/secrets/hcp" 30 ) 31 32 const ( 33 // Name is the unique name of this Enricher. 34 Name = "secrets/hcpidentity" 35 36 version = 1 37 defaultBaseURL = "https://api.cloud.hashicorp.com" 38 ) 39 40 var _ enricher.Enricher = &Enricher{} 41 42 // Enricher augments HCP access tokens with identity metadata. 43 type Enricher struct { 44 baseURL string 45 httpClient *http.Client 46 } 47 48 // New creates a new Enricher with default configuration. 49 func New() enricher.Enricher { 50 return &Enricher{ 51 baseURL: defaultBaseURL, 52 httpClient: http.DefaultClient, 53 } 54 } 55 56 // NewWithBaseURL creates a new Enricher using a custom base URL (for tests). 57 func NewWithBaseURL(baseURL string) enricher.Enricher { 58 return &Enricher{ 59 baseURL: baseURL, 60 httpClient: http.DefaultClient, 61 } 62 } 63 64 // Name of the Enricher. 65 func (Enricher) Name() string { return Name } 66 67 // Version of the Enricher. 68 func (Enricher) Version() int { return version } 69 70 // Requirements of the Enricher (needs network access). 71 func (Enricher) Requirements() *plugin.Capabilities { 72 return &plugin.Capabilities{Network: plugin.NetworkOnline} 73 } 74 75 // RequiredPlugins returns the plugins that are required to be enabled for this Enricher to run. 76 func (Enricher) RequiredPlugins() []string { return []string{} } 77 78 // Enrich augments HCP access tokens with identity metadata obtained from the API. 79 func (e *Enricher) Enrich(ctx context.Context, _ *enricher.ScanInput, inv *inventory.Inventory) error { 80 for _, s := range inv.Secrets { 81 if err := ctx.Err(); err != nil { 82 return err 83 } 84 tok, ok := s.Secret.(hcp.AccessToken) 85 if !ok || tok.Token == "" { 86 continue 87 } 88 // Call caller-identity. Do not fail the entire enrichment if this fails. 89 id, err := e.fetchCallerIdentity(ctx, tok.Token) 90 if err != nil { 91 continue 92 } 93 tok.OrganizationID = id.OrganizationID 94 tok.ProjectID = id.ProjectID 95 tok.PrincipalID = id.PrincipalID 96 tok.PrincipalType = id.PrincipalType 97 tok.ServiceName = id.ServiceName 98 tok.GroupIDs = id.GroupIDs 99 s.Secret = tok 100 } 101 return nil 102 } 103 104 // identityResponse represents the HCP caller-identity endpoint response. 105 // Only some of the fields are included for enrichment. 106 // See https://developer.hashicorp.com/hcp/api-docs/identity#IamService_GetCallerIdentity 107 type identityResponse struct { 108 Principal struct { 109 ID string `json:"id"` 110 Type string `json:"type"` 111 Group []string `json:"group_ids"` 112 User struct { 113 ID string `json:"id"` 114 Email string `json:"email"` 115 } `json:"user"` 116 Service struct { 117 ID string `json:"id"` 118 Name string `json:"name"` 119 Organization string `json:"organization_id"` 120 Project string `json:"project_id"` 121 } `json:"service"` 122 } `json:"principal"` 123 } 124 125 type identity struct { 126 OrganizationID string 127 ProjectID string 128 PrincipalID string 129 PrincipalType string 130 ServiceName string 131 GroupIDs []string 132 UserEmail string 133 UserID string 134 } 135 136 func (e *Enricher) fetchCallerIdentity(ctx context.Context, bearer string) (identity, error) { 137 url := e.baseURL + "/iam/2019-12-10/caller-identity" 138 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 139 if err != nil { 140 return identity{}, fmt.Errorf("create request: %w", err) 141 } 142 req.Header.Set("Authorization", "Bearer "+bearer) 143 res, err := e.httpClient.Do(req) 144 if err != nil { 145 return identity{}, fmt.Errorf("http GET: %w", err) 146 } 147 defer res.Body.Close() 148 if res.StatusCode != http.StatusOK { 149 // Treat non-200 as non-fatal; skip enrichment. 150 _, _ = io.Copy(io.Discard, res.Body) 151 return identity{}, nil 152 } 153 var raw identityResponse 154 if err := json.NewDecoder(res.Body).Decode(&raw); err != nil { 155 return identity{}, fmt.Errorf("decode response: %w", err) 156 } 157 return identity{ 158 OrganizationID: raw.Principal.Service.Organization, 159 ProjectID: raw.Principal.Service.Project, 160 PrincipalID: raw.Principal.ID, 161 PrincipalType: raw.Principal.Type, 162 ServiceName: raw.Principal.Service.Name, 163 GroupIDs: raw.Principal.Group, 164 UserEmail: raw.Principal.User.Email, 165 UserID: raw.Principal.User.ID, 166 }, nil 167 }