From eaad08dc22eb3c7f0196b4c7b7134806577bdfdc Mon Sep 17 00:00:00 2001 From: Ice3man Date: Sat, 31 Aug 2024 00:46:48 +0530 Subject: [PATCH] feat: added vercel support for deployments + domains --- pkg/inventory/inventory.go | 4 + pkg/providers/vercel/api.go | 141 ++++++++++++++++++++ pkg/providers/vercel/api_types.go | 206 ++++++++++++++++++++++++++++++ pkg/providers/vercel/vercel.go | 106 +++++++++++++++ 4 files changed, 457 insertions(+) create mode 100644 pkg/providers/vercel/api.go create mode 100644 pkg/providers/vercel/api_types.go create mode 100644 pkg/providers/vercel/vercel.go diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go index 77c00f5..0fdfb7b 100644 --- a/pkg/inventory/inventory.go +++ b/pkg/inventory/inventory.go @@ -20,6 +20,7 @@ import ( "github.com/projectdiscovery/cloudlist/pkg/providers/openstack" "github.com/projectdiscovery/cloudlist/pkg/providers/scaleway" "github.com/projectdiscovery/cloudlist/pkg/providers/terraform" + "github.com/projectdiscovery/cloudlist/pkg/providers/vercel" "github.com/projectdiscovery/cloudlist/pkg/schema" mapsutil "github.com/projectdiscovery/utils/maps" ) @@ -66,6 +67,7 @@ var Providers = map[string][]string{ "hetzner": hetzner.Services, "openstack": openstack.Services, "kubernetes": k8s.Services, + "vercel": vercel.Services, } func GetProviders() []string { @@ -119,6 +121,8 @@ func nameToProvider(value string, block schema.OptionBlock) (schema.Provider, er return openstack.New(block) case "kubernetes": return k8s.New(block) + case "vercel": + return vercel.New(block) default: return nil, fmt.Errorf("invalid provider name found: %s", value) } diff --git a/pkg/providers/vercel/api.go b/pkg/providers/vercel/api.go new file mode 100644 index 0000000..069ec00 --- /dev/null +++ b/pkg/providers/vercel/api.go @@ -0,0 +1,141 @@ +package vercel + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/pkg/errors" +) + +type vercelClient struct { + config *newClientConfig + url string + httpClient *http.Client +} + +type newClientConfig struct { + Token string + Teamid string +} + +func newAPIClient(config newClientConfig) *vercelClient { + return &vercelClient{ + config: &config, + url: "https://api.vercel.com", + httpClient: &http.Client{ + Transport: &http.Transport{}, + Timeout: 60 * time.Second, + }, + } +} + +type apiRequest struct { + Method string + Path string + Body interface{} + Query url.Values + ResponseTarget interface{} +} + +func newApiRequest(method string, path string, ResponseTarget interface{}) apiRequest { + return apiRequest{ + Method: method, + Path: path, + Body: nil, + Query: url.Values{}, + ResponseTarget: ResponseTarget, + } +} + +// Call the Vercel API and unmarshal its response directly +func (c *vercelClient) Call(req apiRequest) error { + path := req.Path + query := req.Query.Encode() + if query != "" { + path = fmt.Sprintf("%s?%s", path, query) + } + + httpResponse, err := c.request(req.Method, path, req.Body) + if err != nil { + return err + } + defer httpResponse.Body.Close() + if req.ResponseTarget == nil { + return nil + } + err = json.NewDecoder(httpResponse.Body).Decode(req.ResponseTarget) + if err != nil { + return errors.Wrap(err, "unable to decode response body") + } + return nil +} + +// Perform a request and return its response +func (c *vercelClient) request(method string, path string, body interface{}) (*http.Response, error) { + payload, err := marshalBody(body) + if err != nil { + return nil, errors.Wrap(err, "unable to marshal request body") + } + req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.url, path), payload) + if err != nil { + return nil, errors.Wrap(err, "unable to create request") + } + req.Header.Set("User-Agent", "cloudlist-go") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.Token)) + + res, err := c.httpClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "unable to perform request") + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + var responseBody map[string]interface{} + err = json.NewDecoder(res.Body).Decode(&responseBody) + if err != nil { + return nil, fmt.Errorf("response returned status code %d, path: %s", res.StatusCode, path) + } + + // Try to prettyprint the response body + // If that is not possible we return the raw body + pretty, err := json.MarshalIndent(responseBody, "", " ") + if err != nil { + return nil, fmt.Errorf("response returned status code %d: %+v, path: %s", res.StatusCode, responseBody, path) + } + return nil, fmt.Errorf("response returned status code %d: %+v, path: %s", res.StatusCode, string(pretty), path) + } + return res, nil + +} + +// JSON marshal the body if present +func marshalBody(body interface{}) (io.Reader, error) { + var payload io.Reader = nil + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + payload = bytes.NewBuffer(b) + } + return payload, nil +} + +// Endpoints implementations below +// ------------------------------ + +func (c *vercelClient) ListProjects(req ListProjectsRequest) (res ListProjectsResponse, err error) { + apiRequest := newApiRequest("GET", "/v8/projects", &res) + if c.config.Teamid != "" { + apiRequest.Query.Add("teamId", c.config.Teamid) + } + err = c.Call(apiRequest) + if err != nil { + return ListProjectsResponse{}, fmt.Errorf("unable to fetch projects: %w", err) + } + return res, nil +} diff --git a/pkg/providers/vercel/api_types.go b/pkg/providers/vercel/api_types.go new file mode 100644 index 0000000..c6e50c4 --- /dev/null +++ b/pkg/providers/vercel/api_types.go @@ -0,0 +1,206 @@ +package vercel + +type ListProjectsRequest struct { + // Limit the number of projects returned. + // Required: No + Limit int64 `json:"limit,omitempty"` + + // The updatedAt point64where the list should start. + // Required: No + Since int64 `json:"since,omitempty"` + + // The updatedAt point64where the list should end. + // Required: No + Until int64 `json:"until,omitempty"` + + // Search projects by the name field. + // Required: No + Search string `json:"string,omitempty"` +} + +type ListProjectsResponse struct { + Projects []Project `json:"projects"` +} + +// Project houses all the information vercel offers about a project via their api +type Project struct { + Accountid string `json:"accountid"` + Alias []struct { + ConfiguredBy string `json:"configuredBy"` + ConfiguredChangedAt int64 `json:"configuredChangedAt"` + CreatedAt int64 `json:"createdAt"` + Deployment struct { + Alias []string `json:"alias"` + AliasAssigned int64 `json:"aliasAssigned"` + Builds []interface{} `json:"builds"` + CreatedAt int64 `json:"createdAt"` + CreatedIn string `json:"createdIn"` + Creator struct { + Uid string `json:"uid"` + Email string `json:"email"` + Username string `json:"username"` + GithubLogin string `json:"githubLogin"` + } `json:"creator"` + DeploymentHostname string `json:"deploymentHostname"` + Forced bool `json:"forced"` + Id string `json:"id"` + Meta struct { + GithubCommitRef string `json:"githubCommitRef"` + GithubRepo string `json:"githubRepo"` + GithubOrg string `json:"githubOrg"` + GithubCommitSha string `json:"githubCommitSha"` + GithubRepoid string `json:"githubRepoid"` + GithubCommitMessage string `json:"githubCommitMessage"` + GithubCommitAuthorLogin string `json:"githubCommitAuthorLogin"` + GithubDeployment string `json:"githubDeployment"` + GithubCommitOrg string `json:"githubCommitOrg"` + GithubCommitAuthorName string `json:"githubCommitAuthorName"` + GithubCommitRepo string `json:"githubCommitRepo"` + GithubCommitRepoid string `json:"githubCommitRepoid"` + } `json:"meta"` + Name string `json:"name"` + Plan string `json:"plan"` + Private bool `json:"private"` + ReadyState string `json:"readyState"` + Target string `json:"target"` + Teamid string `json:"teamid"` + Type string `json:"type"` + URL string `json:"url"` + Userid string `json:"userid"` + WithCache bool `json:"withCache"` + } `json:"deployment"` + Domain string `json:"domain"` + Environment string `json:"environment"` + Target string `json:"target"` + } `json:"alias"` + Analytics struct { + Id string `json:"id"` + EnabledAt int64 `json:"enabledAt"` + DisabledAt int64 `json:"disabledAt"` + CanceledAt int64 `json:"canceledAt"` + } `json:"analytics"` + AutoExposeSystemEnvs bool `json:"autoExposeSystemEnvs"` + BuildCommand string `json:"buildCommand"` + CreatedAt int64 `json:"createdAt"` + DevCommand string `json:"devCommand"` + DirectoryListing bool `json:"directoryListing"` + Env []struct { + Type string `json:"type"` + Id string `json:"id"` + Key string `json:"key"` + Value string `json:"value"` + Target []string `json:"target"` + Configurationid interface{} `json:"configurationid"` + UpdatedAt int64 `json:"updatedAt"` + CreatedAt int64 `json:"createdAt"` + } `json:"env"` + Framework string `json:"framework"` + Id string `json:"id"` + InstallCommand string `json:"installCommand"` + Name string `json:"name"` + NodeVersion string `json:"nodeVersion"` + OutputDirectory string `json:"outputDirectory"` + PublicSource bool `json:"publicSource"` + RootDirectory string `json:"rootDirectory"` + ServerlessFunctionRegion string `json:"serverlessFunctionRegion"` + SourceFilesOutsideRootDirectory bool `json:"sourceFilesOutsideRootDirectory"` + UpdatedAt int64 `json:"updatedAt"` + Link struct { + Type string `json:"type"` + Repo string `json:"repo"` + Repoid int64 `json:"repoid"` + Org string `json:"org"` + GitCredentialid string `json:"gitCredentialid"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` + Sourceless bool `json:"sourceless"` + ProductionBranch string `json:"productionBranch"` + DeployHooks []interface{} `json:"deployHooks"` + ProjectName string `json:"projectName"` + ProjectNamespace string `json:"projectNamespace"` + Owner string `json:"owner"` + Slug string `json:"slug"` + } `json:"link"` + LatestDeployments []struct { + Alias []string `json:"alias"` + AliasAssigned int64 `json:"aliasAssigned"` + Builds []interface{} `json:"builds"` + CreatedAt int64 `json:"createdAt"` + CreatedIn string `json:"createdIn"` + Creator struct { + Uid string `json:"uid"` + Email string `json:"email"` + Username string `json:"username"` + GithubLogin string `json:"githubLogin"` + } `json:"creator"` + DeploymentHostname string `json:"deploymentHostname"` + Forced bool `json:"forced"` + Id string `json:"id"` + Meta struct { + GithubCommitRef string `json:"githubCommitRef"` + GithubRepo string `json:"githubRepo"` + GithubOrg string `json:"githubOrg"` + GithubCommitSha string `json:"githubCommitSha"` + GithubCommitAuthorLogin string `json:"githubCommitAuthorLogin"` + GithubCommitMessage string `json:"githubCommitMessage"` + GithubRepoid string `json:"githubRepoid"` + GithubDeployment string `json:"githubDeployment"` + GithubCommitOrg string `json:"githubCommitOrg"` + GithubCommitAuthorName string `json:"githubCommitAuthorName"` + GithubCommitRepo string `json:"githubCommitRepo"` + GithubCommitRepoid string `json:"githubCommitRepoid"` + } `json:"meta"` + Name string `json:"name"` + Plan string `json:"plan"` + Private bool `json:"private"` + ReadyState string `json:"readyState"` + Target interface{} `json:"target"` + Teamid string `json:"teamid"` + Type string `json:"type"` + URL string `json:"url"` + Userid string `json:"userid"` + WithCache bool `json:"withCache"` + } `json:"latestDeployments"` + Targets struct { + Production struct { + Alias []string `json:"alias"` + AliasAssigned int64 `json:"aliasAssigned"` + Builds []interface{} `json:"builds"` + CreatedAt int64 `json:"createdAt"` + CreatedIn string `json:"createdIn"` + Creator struct { + Uid string `json:"uid"` + Email string `json:"email"` + Username string `json:"username"` + GithubLogin string `json:"githubLogin"` + } `json:"creator"` + DeploymentHostname string `json:"deploymentHostname"` + Forced bool `json:"forced"` + Id string `json:"id"` + Meta struct { + GithubCommitRef string `json:"githubCommitRef"` + GithubRepo string `json:"githubRepo"` + GithubOrg string `json:"githubOrg"` + GithubCommitSha string `json:"githubCommitSha"` + GithubRepoid string `json:"githubRepoid"` + GithubCommitMessage string `json:"githubCommitMessage"` + GithubCommitAuthorLogin string `json:"githubCommitAuthorLogin"` + GithubDeployment string `json:"githubDeployment"` + GithubCommitOrg string `json:"githubCommitOrg"` + GithubCommitAuthorName string `json:"githubCommitAuthorName"` + GithubCommitRepo string `json:"githubCommitRepo"` + GithubCommitRepoid string `json:"githubCommitRepoid"` + } `json:"meta"` + Name string `json:"name"` + Plan string `json:"plan"` + Private bool `json:"private"` + ReadyState string `json:"readyState"` + Target string `json:"target"` + Teamid string `json:"teamid"` + Type string `json:"type"` + URL string `json:"url"` + Userid string `json:"userid"` + WithCache bool `json:"withCache"` + } `json:"production"` + } `json:"targets"` +} diff --git a/pkg/providers/vercel/vercel.go b/pkg/providers/vercel/vercel.go new file mode 100644 index 0000000..48aecd4 --- /dev/null +++ b/pkg/providers/vercel/vercel.go @@ -0,0 +1,106 @@ +package vercel + +import ( + "context" + "strings" + + "github.com/projectdiscovery/cloudlist/pkg/schema" +) + +var Services = []string{"deployments", "domains"} + +// Provider is a data provider for vercel API +type Provider struct { + id string + client *vercelClient + services schema.ServiceMap +} + +// New creates a new provider client for vercel API +func New(options schema.OptionBlock) (*Provider, error) { + accessKey, ok := options.GetMetadata(apiAccessToken) + if !ok { + return nil, &schema.ErrNoSuchKey{Name: apiAccessToken} + } + teamID, _ := options.GetMetadata(apiTeamID) + + id, _ := options.GetMetadata("id") + supportedServicesMap := make(map[string]struct{}) + for _, s := range Services { + supportedServicesMap[s] = struct{}{} + } + services := make(schema.ServiceMap) + if ss, ok := options.GetMetadata("services"); ok { + for _, s := range strings.Split(ss, ",") { + if _, ok := supportedServicesMap[s]; ok { + services[s] = struct{}{} + } + } + } + if len(services) == 0 { + for _, s := range Services { + services[s] = struct{}{} + } + } + + client := newAPIClient(newClientConfig{ + Token: accessKey, + Teamid: teamID, + }) + return &Provider{client: client, id: id, services: services}, nil +} + +const providerName = "vercel" + +// Name returns the name of the provider +func (p *Provider) Name() string { + return providerName +} + +// ID returns the name of the provider id +func (p *Provider) ID() string { + return p.id +} + +// Services returns the provider services +func (p *Provider) Services() []string { + return p.services.Keys() +} + +const apiAccessToken = "vercel_api_token" +const apiTeamID = "vercel_team_id" + +// Resources returns the provider for an resource deployment source. +func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) { + finalResources := schema.NewResources() + projects, err := p.client.ListProjects(ListProjectsRequest{}) + if err != nil { + return nil, err + } + for _, project := range projects.Projects { + if p.services.Has("deployments") { + for _, deployment := range project.LatestDeployments { + finalResources.Append(&schema.Resource{ + Public: !deployment.Private, + Provider: providerName, + Service: "deployments", + ID: p.id, + DNSName: deployment.URL, + }) + } + } + + for _, target := range project.Targets.Production.Alias { + if p.services.Has("domains") { + finalResources.Append(&schema.Resource{ + Public: true, + Provider: providerName, + Service: "domains", + ID: p.id, + DNSName: target, + }) + } + } + } + return finalResources, nil +}