diff --git a/PROVIDERS.md b/PROVIDERS.md index fc5dda9e..41e753c8 100644 --- a/PROVIDERS.md +++ b/PROVIDERS.md @@ -481,3 +481,32 @@ The `dnssimple_api_token` can be generated from the DNSSimple account settings u References - 1. https://developer.dnsimple.com/v2/ 2. https://support.dnsimple.com/articles/api-access-token/ + +### OVH + +Cloudlist supports fetching DNS records from OVH. + +- **Provider key**: `ovh` +- **Services**: `dns` +- **id**: An arbitrary label you choose to tag resources. It helps distinguish multiple OVH accounts/configs. +- **Required auth**: + - `application_key` + - `application_secret` + - `consumer_key` +- **Endpoint**: OVH API endpoint. Defaults to `ovh-eu` if omitted. Common values: `ovh-eu`, `ovh-ca`, `ovh-us`. + +Configuration example (from `ovh.yaml`): + +```yaml +- provider: ovh + id: ovh-prod + endpoint: ovh-ca + application_key: $OVH_APP_KEY + application_secret: $OVH_APP_SECRET + consumer_key: $OVH_CONSUMER_KEY +``` + +References - +1. https://eu.api.ovh.com/console/?section=%2Fdomain&branch=v1 +2. https://api.ovh.com/createToken/ +3. https://help.ovhcloud.com/csm/en-gb-api-getting-started-ovhcloud-api diff --git a/go.mod b/go.mod index ac6709cb..82838bbe 100644 --- a/go.mod +++ b/go.mod @@ -165,7 +165,7 @@ require ( google.golang.org/protobuf v1.36.6 gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect @@ -214,6 +214,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/mholt/archives v0.1.0 // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 // indirect + github.com/ovh/go-ovh v1.9.0 // indirect github.com/projectdiscovery/fastdialer v0.4.3 // indirect github.com/projectdiscovery/hmap v0.0.92 // indirect github.com/projectdiscovery/retryabledns v1.0.104 // indirect diff --git a/go.sum b/go.sum index 4da1a163..516fc0fd 100644 --- a/go.sum +++ b/go.sum @@ -488,6 +488,8 @@ github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKi github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= +github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -980,6 +982,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go index b126ffb1..be642084 100644 --- a/pkg/inventory/inventory.go +++ b/pkg/inventory/inventory.go @@ -21,6 +21,7 @@ import ( "github.com/projectdiscovery/cloudlist/pkg/providers/namecheap" "github.com/projectdiscovery/cloudlist/pkg/providers/nomad" "github.com/projectdiscovery/cloudlist/pkg/providers/openstack" + "github.com/projectdiscovery/cloudlist/pkg/providers/ovh" "github.com/projectdiscovery/cloudlist/pkg/providers/scaleway" "github.com/projectdiscovery/cloudlist/pkg/providers/terraform" "github.com/projectdiscovery/cloudlist/pkg/schema" @@ -70,6 +71,7 @@ var Providers = map[string][]string{ "nomad": nomad.Services, "hetzner": hetzner.Services, "openstack": openstack.Services, + "ovh": ovh.Services, "kubernetes": k8s.Services, "custom": custom.Services, } @@ -125,6 +127,8 @@ func nameToProvider(value string, block schema.OptionBlock) (schema.Provider, er return hetzner.New(block) case "openstack": return openstack.New(block) + case "ovh": + return ovh.New(block) case "kubernetes": return k8s.New(block) case "custom": diff --git a/pkg/providers/ovh/dns.go b/pkg/providers/ovh/dns.go new file mode 100644 index 00000000..0e91f079 --- /dev/null +++ b/pkg/providers/ovh/dns.go @@ -0,0 +1,87 @@ +package ovh + +import ( + "context" + "fmt" + "strings" + + "github.com/ovh/go-ovh/ovh" + "github.com/projectdiscovery/cloudlist/pkg/schema" +) + +type dnsProvider struct { + id string + client *ovh.Client +} + +func (d *dnsProvider) name() string { return "dns" } + +type ovhRecord struct { + ID int64 `json:"id"` + FieldType string `json:"fieldType"` + SubDomain string `json:"subDomain"` + Target string `json:"target"` + TTL int `json:"ttl"` +} + +// GetResource returns all DNS resources from OVH +func (d *dnsProvider) GetResource(ctx context.Context) (*schema.Resources, error) { + list := schema.NewResources() + + var zones []string + if err := d.client.GetWithContext(ctx, "/domain/zone", &zones); err != nil { + return nil, err + } + + recordTypes := []string{"A", "AAAA", "CNAME"} + for _, zone := range zones { + for _, rt := range recordTypes { + var ids []int64 + path := fmt.Sprintf("/domain/zone/%s/record?fieldType=%s", zone, rt) + if err := d.client.GetWithContext(ctx, path, &ids); err != nil { + continue + } + for _, id := range ids { + var rec ovhRecord + if err := d.client.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%d", zone, id), &rec); err != nil { + continue + } + + name := strings.TrimSpace(rec.SubDomain) + var fqdn string + if name == "" || name == "@" { + fqdn = zone + } else { + fqdn = name + "." + zone + } + + // Append DNS name (public) + list.Append(&schema.Resource{ + Public: true, + Provider: providerName, + DNSName: fqdn, + ID: d.id, + Service: d.name(), + }) + + // For A/AAAA also append IP resource; skip CNAME target to avoid duplicates + res := &schema.Resource{ + Public: true, + Provider: providerName, + ID: d.id, + Service: d.name(), + } + + if strings.ToUpper(rec.FieldType) == "A" { + res.PublicIPv4 = rec.Target + } else if strings.ToUpper(rec.FieldType) == "AAAA" { + res.PublicIPv6 = rec.Target + } + + list.Append(res) + } + } + } + + return list, nil +} diff --git a/pkg/providers/ovh/ovh.go b/pkg/providers/ovh/ovh.go new file mode 100644 index 00000000..c317e415 --- /dev/null +++ b/pkg/providers/ovh/ovh.go @@ -0,0 +1,95 @@ +package ovh + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/ovh/go-ovh/ovh" + "github.com/projectdiscovery/cloudlist/pkg/schema" +) + +const providerName = "ovh" + +var Services = []string{"dns"} + +type Provider struct { + id string + client *ovh.Client + httpClient *http.Client + services schema.ServiceMap +} + +func New(options schema.OptionBlock) (*Provider, error) { + id, _ := options.GetMetadata("id") + + // service selection + supported := map[string]struct{}{"dns": {}} + services := make(schema.ServiceMap) + if ss, ok := options.GetMetadata("services"); ok { + for _, s := range strings.Split(ss, ",") { + s = strings.TrimSpace(s) + if _, ok := supported[s]; ok { + services[s] = struct{}{} + } + } + } + if len(services) == 0 { + for _, s := range Services { + services[s] = struct{}{} + } + } + + // OVH endpoint (default ovh-eu) + endpoint := "ovh-eu" + if ep, ok := options.GetMetadata("endpoint"); ok && ep != "" { + endpoint = ep + } + + // Credentials + appKey, ok := options.GetMetadata("application_key") + if !ok || appKey == "" { + return nil, &schema.ErrNoSuchKey{Name: "application_key"} + } + appSecret, ok := options.GetMetadata("application_secret") + if !ok || appSecret == "" { + return nil, &schema.ErrNoSuchKey{Name: "application_secret"} + } + consumerKey, ok := options.GetMetadata("consumer_key") + if !ok || consumerKey == "" { + return nil, &schema.ErrNoSuchKey{Name: "consumer_key"} + } + + cli, err := ovh.NewClient(endpoint, appKey, appSecret, consumerKey) + if err != nil { + return nil, err + } + + httpClient := &http.Client{Timeout: 30 * time.Second} + cli.Client = httpClient + + return &Provider{ + id: id, + client: cli, + httpClient: httpClient, + services: services, + }, nil +} + +func (p *Provider) Name() string { return providerName } +func (p *Provider) ID() string { return p.id } +func (p *Provider) Services() []string { return p.services.Keys() } + +func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) { + out := schema.NewResources() + if p.services.Has("dns") { + d := &dnsProvider{id: p.id, client: p.client} + r, err := d.GetResource(ctx) + if err != nil { + return nil, err + } + out.Merge(r) + } + return out, nil +}