Skip to content

Commit 42d4a79

Browse files
committed
Added pugrecon.com as a subdomains source
1 parent 678fbc8 commit 42d4a79

4 files changed

Lines changed: 156 additions & 1 deletion

File tree

v2/pkg/passive/sources.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/chinaz"
2121
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/commoncrawl"
2222
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/crtsh"
23+
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/digitalyama"
2324
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/digitorus"
2425
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/dnsdb"
2526
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/dnsdumpster"
@@ -34,6 +35,7 @@ import (
3435
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/intelx"
3536
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/leakix"
3637
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/netlas"
38+
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/pugrecon"
3739
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/quake"
3840
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/rapiddns"
3941
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/redhuntlabs"
@@ -47,7 +49,6 @@ import (
4749
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/waybackarchive"
4850
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/whoisxmlapi"
4951
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/zoomeyeapi"
50-
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping/sources/digitalyama"
5152
mapsutil "github.com/projectdiscovery/utils/maps"
5253
)
5354

@@ -77,6 +78,7 @@ var AllSources = [...]subscraping.Source{
7778
&netlas.Source{},
7879
&leakix.Source{},
7980
&quake.Source{},
81+
&pugrecon.Source{},
8082
&rapiddns.Source{},
8183
&redhuntlabs.Source{},
8284
// &riddler.Source{}, // failing due to cloudfront protection

v2/pkg/passive/sources_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var (
3434
"intelx",
3535
"netlas",
3636
"quake",
37+
"pugrecon",
3738
"rapiddns",
3839
"redhuntlabs",
3940
// "riddler", // failing due to cloudfront protection

v2/pkg/runner/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ func (options *Options) preProcessDomains() {
244244
var defaultRateLimits = []string{
245245
"github=30/m",
246246
"fullhunt=60/m",
247+
"pugrecon=10/s",
247248
fmt.Sprintf("robtex=%d/ms", uint(math.MaxUint)),
248249
"securitytrails=1/s",
249250
"shodan=1/s",
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Package pugrecon logic
2+
package pugrecon
3+
4+
import (
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"time"
11+
12+
"github.com/projectdiscovery/subfinder/v2/pkg/subscraping"
13+
)
14+
15+
// pugreconResult stores a single result from the pugrecon API
16+
type pugreconResult struct {
17+
Name string `json:"name"`
18+
}
19+
20+
// pugreconAPIResponse stores the response from the pugrecon API
21+
type pugreconAPIResponse struct {
22+
Results []pugreconResult `json:"results"`
23+
QuotaRemaining int `json:"quota_remaining"`
24+
Limited bool `json:"limited"`
25+
TotalResults int `json:"total_results"`
26+
Message string `json:"message"`
27+
}
28+
29+
// Source is the passive scraping agent
30+
type Source struct {
31+
apiKeys []string
32+
timeTaken time.Duration
33+
errors int
34+
results int
35+
skipped bool
36+
}
37+
38+
// Run function returns all subdomains found with the service
39+
func (s *Source) Run(ctx context.Context, domain string, session *subscraping.Session) <-chan subscraping.Result {
40+
results := make(chan subscraping.Result)
41+
s.errors = 0
42+
s.results = 0
43+
44+
go func() {
45+
defer func(startTime time.Time) {
46+
s.timeTaken = time.Since(startTime)
47+
close(results)
48+
}(time.Now())
49+
50+
randomApiKey := subscraping.PickRandom(s.apiKeys, s.Name())
51+
if randomApiKey == "" {
52+
s.skipped = true
53+
return
54+
}
55+
56+
// Prepare POST request data
57+
postData := map[string]string{"domain_name": domain}
58+
bodyBytes, err := json.Marshal(postData)
59+
if err != nil {
60+
results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: fmt.Errorf("failed to marshal request body: %w", err)}
61+
s.errors++
62+
return
63+
}
64+
bodyReader := bytes.NewReader(bodyBytes)
65+
66+
// Prepare headers
67+
headers := map[string]string{
68+
"Authorization": "Bearer " + randomApiKey,
69+
"Content-Type": "application/json",
70+
"Accept": "application/json",
71+
}
72+
73+
apiURL := "https://pugrecon.com/api/v1/domains"
74+
resp, err := session.HTTPRequest(ctx, http.MethodPost, apiURL, "", headers, bodyReader, subscraping.BasicAuth{}) // Use HTTPRequest for full header control
75+
if err != nil {
76+
results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: err}
77+
s.errors++
78+
session.DiscardHTTPResponse(resp)
79+
return
80+
}
81+
defer resp.Body.Close()
82+
83+
if resp.StatusCode != http.StatusOK {
84+
errorMsg := fmt.Sprintf("received status code %d", resp.StatusCode)
85+
// Attempt to read error message from body if possible
86+
var apiResp pugreconAPIResponse
87+
if json.NewDecoder(resp.Body).Decode(&apiResp) == nil && apiResp.Message != "" {
88+
errorMsg = fmt.Sprintf("%s: %s", errorMsg, apiResp.Message)
89+
}
90+
results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: fmt.Errorf(errorMsg)}
91+
s.errors++
92+
return
93+
}
94+
95+
var response pugreconAPIResponse
96+
err = json.NewDecoder(resp.Body).Decode(&response)
97+
if err != nil {
98+
results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: err}
99+
s.errors++
100+
return
101+
}
102+
103+
if response.Message != "" && !response.Limited { // Handle potential non-error messages, except rate limit info
104+
// Log or handle message if needed, but don't treat as hard error unless necessary
105+
}
106+
107+
for _, subdomain := range response.Results {
108+
results <- subscraping.Result{
109+
Source: s.Name(), Type: subscraping.Subdomain, Value: subdomain.Name,
110+
}
111+
s.results++
112+
}
113+
}()
114+
115+
return results
116+
}
117+
118+
// Name returns the name of the source
119+
func (s *Source) Name() string {
120+
return "pugrecon"
121+
}
122+
123+
// IsDefault returns false as this is not a default source.
124+
func (s *Source) IsDefault() bool {
125+
return false
126+
}
127+
128+
// HasRecursiveSupport returns false as this source does not support recursive searches.
129+
func (s *Source) HasRecursiveSupport() bool {
130+
return false
131+
}
132+
133+
// NeedsKey returns true as this source requires an API key.
134+
func (s *Source) NeedsKey() bool {
135+
return true
136+
}
137+
138+
// AddApiKeys adds the API keys for the source.
139+
func (s *Source) AddApiKeys(keys []string) {
140+
s.apiKeys = keys
141+
}
142+
143+
// Statistics returns the statistics for the source.
144+
func (s *Source) Statistics() subscraping.Statistics {
145+
return subscraping.Statistics{
146+
Errors: s.errors,
147+
Results: s.results,
148+
TimeTaken: s.timeTaken,
149+
Skipped: s.skipped,
150+
}
151+
}

0 commit comments

Comments
 (0)