Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Google Cloud Platform supports **two discovery approaches** and **two authentica
- `service_account_email` (string, required if short-lived): Target service account to impersonate
- `source_credentials` (string, optional): Path to source credentials file (uses ADC if not provided)
- `token_lifetime` (string, optional): Token lifetime in seconds (e.g., "3600s") or Go duration format (e.g., "1h"). Range: 1s to 3600s (1 hour). Default: "3600s"
- `project_ids` (list, optional): Comma-separated/list of project IDs to enumerate. When provided, Cloudlist skips discovery in every other accessible project, both for individual APIs and the organization-level Asset API.

---

Expand Down Expand Up @@ -191,8 +192,13 @@ Google Cloud Platform supports **two discovery approaches** and **two authentica
use_short_lived_credentials: true
service_account_email: "asset-viewer-sa@project.iam.gserviceaccount.com"
token_lifetime: "7200s" # 2 hours
project_ids:
- security-core
- shared-infra
```

Add `project_ids` to either configuration style to limit enumeration strictly to the listed projects (Cloud Asset API requests are filtered too), which is helpful for large organizations or delegated-access service accounts.

**Required Organization-Level Roles:**
1. `roles/cloudasset.viewer` - Core Asset API access
2. `roles/resourcemanager.viewer` - List projects in organization
Expand Down
78 changes: 67 additions & 11 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"os"
"strconv"
"strings"
"sync"

jsoniter "github.com/json-iterator/go"
"github.com/projectdiscovery/cloudlist/pkg/inventory"
"github.com/projectdiscovery/cloudlist/pkg/schema"
"github.com/projectdiscovery/cloudlist/pkg/schema/validate"
"github.com/projectdiscovery/gologger"
)

Expand Down Expand Up @@ -115,6 +117,8 @@ func (r *Runner) Enumerate() {
continue
}

sanitizePrivateIPs(instance, r.options.ExcludePrivate)

builder.Reset()

if r.options.JSON {
Expand All @@ -124,7 +128,7 @@ func (r *Runner) Enumerate() {
} else {
builder.Write(data)
builder.WriteString("\n")
output.Write(builder.Bytes()) //nolint
writeOutputBytes(output, builder.Bytes())

if instance.DNSName != "" {
hostsCount++
Expand Down Expand Up @@ -152,7 +156,7 @@ func (r *Runner) Enumerate() {
hostsCount++
builder.WriteString(instance.DNSName)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.DNSName)
}
Expand All @@ -163,31 +167,31 @@ func (r *Runner) Enumerate() {
ipCount++
builder.WriteString(instance.PublicIPv4)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PublicIPv4)
}
if instance.PublicIPv6 != "" {
ipCount++
builder.WriteString(instance.PublicIPv6)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PublicIPv6)
}
if instance.PrivateIpv4 != "" && !r.options.ExcludePrivate {
ipCount++
builder.WriteString(instance.PrivateIpv4)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PrivateIpv4)
}
if instance.PrivateIpv6 != "" && !r.options.ExcludePrivate {
ipCount++
builder.WriteString(instance.PrivateIpv6)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PrivateIpv6)
}
Expand All @@ -198,39 +202,39 @@ func (r *Runner) Enumerate() {
hostsCount++
builder.WriteString(instance.DNSName)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.DNSName)
}
if instance.PublicIPv4 != "" {
ipCount++
builder.WriteString(instance.PublicIPv4)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PublicIPv4)
}
if instance.PublicIPv6 != "" {
ipCount++
builder.WriteString(instance.PublicIPv6)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PublicIPv6)
}
if instance.PrivateIpv4 != "" && !r.options.ExcludePrivate {
ipCount++
builder.WriteString(instance.PrivateIpv4)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PrivateIpv4)
}
if instance.PrivateIpv6 != "" && !r.options.ExcludePrivate {
ipCount++
builder.WriteString(instance.PrivateIpv6)
builder.WriteRune('\n')
output.WriteString(builder.String()) //nolint
writeOutputString(output, builder.String())
builder.Reset()
gologger.Silent().Msgf("%s", instance.PrivateIpv6)
}
Expand Down Expand Up @@ -263,3 +267,55 @@ func Contains(s []string, e string) bool {
}
return false
}

func sanitizePrivateIPs(resource *schema.Resource, exclude bool) {
if !exclude || resource == nil {
return
}
resource.PrivateIpv4 = ""
resource.PrivateIpv6 = ""

if isPrivateIP(resource.PublicIPv4) {
resource.PublicIPv4 = ""
}
if isPrivateIP(resource.PublicIPv6) {
resource.PublicIPv6 = ""
}
}

var (
ipValidatorOnce sync.Once
ipValidator *validate.Validator
)

func isPrivateIP(ip string) bool {
if ip == "" {
return false
}
ipValidatorOnce.Do(func() {
var err error
ipValidator, err = validate.NewValidator()
if err != nil {
gologger.Warning().Msgf("could not initialize ip validator: %s", err)
}
})
if ipValidator == nil {
return false
}
resourceType := ipValidator.Identify(ip)
return resourceType == validate.PrivateIPv4 || resourceType == validate.PrivateIPv6
}

func writeOutputString(output *os.File, data string) {
if output == nil || data == "" {
return
}
_, _ = output.WriteString(data)
}

func writeOutputBytes(output *os.File, data []byte) {
if output == nil || len(data) == 0 {
return
}
_, _ = output.Write(data)
}
58 changes: 58 additions & 0 deletions internal/runner/runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package runner

import (
"testing"

"github.com/projectdiscovery/cloudlist/pkg/schema"
)

func TestSanitizePrivateIPs(t *testing.T) {
resource := &schema.Resource{
PublicIPv4: "1.2.3.4",
PrivateIpv4: "10.0.0.5",
PrivateIpv6: "fd00::1",
PublicIPv6: "2606:4700:4700::1111",
DNSName: "example.internal",
}

sanitizePrivateIPs(resource, true)

if resource.PrivateIpv4 != "" || resource.PrivateIpv6 != "" {
t.Fatalf("expected private addresses to be cleared, got %q and %q", resource.PrivateIpv4, resource.PrivateIpv6)
}

if resource.PublicIPv4 == "" || resource.PublicIPv6 == "" || resource.DNSName == "" {
t.Fatalf("public fields should remain untouched: %+v", resource)
}

resource.PrivateIpv4 = "10.0.0.5"
resource.PrivateIpv6 = "fd00::1"
sanitizePrivateIPs(resource, false)

if resource.PrivateIpv4 == "" || resource.PrivateIpv6 == "" {
t.Fatalf("expected private addresses to remain when exclusion disabled")
}

sanitizePrivateIPs(nil, true)
}

func TestSanitizePrivateIPsOnMisclassifiedFields(t *testing.T) {
resource := &schema.Resource{
PublicIPv4: "10.10.0.5",
PublicIPv6: "fd00::5",
}

sanitizePrivateIPs(resource, true)

if resource.PublicIPv4 != "" || resource.PublicIPv6 != "" {
t.Fatalf("expected private addresses in public fields to be cleared, got ipv4=%q ipv6=%q", resource.PublicIPv4, resource.PublicIPv6)
}

resource.PublicIPv4 = "8.8.8.8"
resource.PublicIPv6 = "2606:4700:4700::1111"
sanitizePrivateIPs(resource, true)

if resource.PublicIPv4 == "" || resource.PublicIPv6 == "" {
t.Fatalf("expected public addresses to remain when exclusion enabled")
}
}
Loading