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  }