Skip to content

Commit 1ebd971

Browse files
author
sw33tLie
committed
website /find endpoint
1 parent b37626a commit 1ebd971

7 files changed

Lines changed: 192 additions & 71 deletions

File tree

docs/src/web/api.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,30 @@ curl "https://your-instance.com/api/v1/targets/wildcards?platform=h1&format=json
6161
curl "https://your-instance.com/api/v1/targets/urls?scope=out"
6262
```
6363

64+
## Find
65+
66+
```
67+
GET /api/v1/find
68+
```
69+
70+
Find programs whose scope matches a given hostname or domain. Automatically expands to root domain matching (e.g. `aaa.example.com` will also match programs scoping `bbb.example.com` via the shared root `example.com`). Cloud provider domains (e.g. `azurewebsites.net`, `herokuapp.com`) are excluded from root domain expansion to avoid false positives.
71+
72+
Query parameters:
73+
74+
| Param | Description |
75+
|-------|-------------|
76+
| `q` | Search query (hostname, domain, etc.) — **required** |
77+
78+
### Examples
79+
80+
```bash
81+
# Find programs with example.com in scope
82+
curl "https://your-instance.com/api/v1/find?q=example.com"
83+
84+
# Find via subdomain — expands to root domain matching
85+
curl "https://your-instance.com/api/v1/find?q=sub.example.com"
86+
```
87+
6488
## Updates
6589

6690
```

pkg/storage/storage.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,38 @@ func (d *DB) GetStats(ctx context.Context, bbpFilter string) ([]PlatformStats, e
15231523
return stats, rows.Err()
15241524
}
15251525

1526+
// FindProgramsByDomain searches for programs that have targets matching the given query string.
1527+
// It returns deduplicated programs (by URL) with their platform, handle, and URL.
1528+
// Only active, non-ignored programs are returned.
1529+
func (d *DB) FindProgramsByDomain(ctx context.Context, query string) ([]ProgramMatch, error) {
1530+
likeQuery := fmt.Sprintf("%%%s%%", query)
1531+
1532+
rows, err := d.sql.QueryContext(ctx, `
1533+
SELECT DISTINCT p.platform, p.handle, p.url
1534+
FROM targets_raw t
1535+
JOIN programs p ON t.program_id = p.id
1536+
WHERE p.disabled = 0
1537+
AND p.is_ignored = 0
1538+
AND t.in_scope = 1
1539+
AND LOWER(t.target) LIKE LOWER($1)
1540+
ORDER BY p.platform, p.handle
1541+
`, likeQuery)
1542+
if err != nil {
1543+
return nil, err
1544+
}
1545+
defer rows.Close()
1546+
1547+
var out []ProgramMatch
1548+
for rows.Next() {
1549+
var m ProgramMatch
1550+
if err := rows.Scan(&m.Platform, &m.Handle, &m.URL); err != nil {
1551+
return nil, err
1552+
}
1553+
out = append(out, m)
1554+
}
1555+
return out, rows.Err()
1556+
}
1557+
15261558
func (d *DB) SearchTargets(ctx context.Context, searchTerm string) ([]Entry, error) {
15271559
likeQuery := fmt.Sprintf("%%%s%%", searchTerm)
15281560

pkg/storage/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ type TargetItem struct {
7373
Variants []TargetVariant
7474
}
7575

76+
// ProgramMatch represents a program that matched a domain search query.
77+
type ProgramMatch struct {
78+
Platform string
79+
Handle string
80+
URL string
81+
}
82+
7683
// TargetVariant captures a requested expansion for a target.
7784
type TargetVariant struct {
7885
Value string
Lines changed: 15 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,37 @@
11
// 1. Get the requested hostname from Burp's requestResponse object
22
String requestHost = requestResponse.httpService().host();
33

4-
// --- ROOT DOMAIN EXTRACTOR ---
5-
// This safely extracts the base domain (e.g., "codebig2.net" from "sub.codebig2.net")
6-
// and accounts for common compound TLDs (like "example.co.uk").
7-
java.util.function.Function<String, String> extractRoot = (host) -> {
8-
if (host == null) return "";
9-
String[] parts = host.split("\\.");
10-
if (parts.length <= 2) return host;
11-
12-
String tld = parts[parts.length - 1];
13-
String sld = parts[parts.length - 2];
14-
15-
// Check for common compound TLDs (e.g., .co.uk, .com.au)
16-
if ((tld.length() == 2 && (sld.equals("co") || sld.equals("com") || sld.equals("org") || sld.equals("net"))) || (sld.length() <= 3 && tld.length() == 2)) {
17-
if (parts.length >= 3) {
18-
return parts[parts.length - 3] + "." + sld + "." + tld;
19-
}
20-
}
21-
return sld + "." + tld;
22-
};
23-
24-
String requestRoot = extractRoot.apply(requestHost);
25-
// -----------------------------
26-
27-
// 2. Fetch the JSON from BBScope
4+
// 2. Query the BBScope Find API
285
StringBuilder jsonResponse = new StringBuilder();
296
try {
30-
java.net.URL url = new java.net.URL("https://bbscope.com/api/v1/programs");
7+
java.net.URL url = new java.net.URL("https://bbscope.com/api/v1/find?q=" + java.net.URLEncoder.encode(requestHost, "UTF-8"));
318
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
329
conn.setRequestMethod("GET");
3310
conn.setRequestProperty("User-Agent", "BurpSuite-BBScope-Action");
34-
11+
3512
java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream()));
3613
String inputLine;
3714
while ((inputLine = in.readLine()) != null) {
3815
jsonResponse.append(inputLine);
3916
}
4017
in.close();
4118
} catch (Exception e) {
42-
logging().logToOutput("Error fetching JSON: " + e.getMessage());
19+
logging().logToOutput("Error fetching from BBScope API: " + e.getMessage());
4320
}
4421

22+
// 3. Extract program URLs from response
4523
String json = jsonResponse.toString();
46-
String foundProgramUrl = null;
47-
48-
// 3. Parse the JSON using Regex
49-
java.util.regex.Pattern programPattern = java.util.regex.Pattern.compile("\"url\":\"([^\"]+)\"[^}]+?\"targets\":\\[(.*?)\\]");
50-
java.util.regex.Matcher programMatcher = programPattern.matcher(json);
51-
52-
while (programMatcher.find()) {
53-
String programUrl = programMatcher.group(1);
54-
String targetsString = programMatcher.group(2);
24+
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile("\"url\":\"([^\"]+)\"");
25+
java.util.regex.Matcher urlMatcher = urlPattern.matcher(json);
5526

56-
// Extract individual target URLs from the array
57-
java.util.regex.Pattern targetUrlPattern = java.util.regex.Pattern.compile("\"([^\"]+)\"");
58-
java.util.regex.Matcher targetUrlMatcher = targetUrlPattern.matcher(targetsString);
59-
60-
while (targetUrlMatcher.find()) {
61-
String rawTargetUrl = targetUrlMatcher.group(1);
62-
try {
63-
// Parse the host out of the target URL
64-
java.net.URI uri = new java.net.URI(rawTargetUrl);
65-
String targetHost = uri.getHost();
66-
67-
if (targetHost != null) {
68-
String targetRoot = extractRoot.apply(targetHost);
69-
70-
// Broad Check: Do they share the same root domain?
71-
// e.g., requestRoot (codebig2.net) == targetRoot (codebig2.net)
72-
if (requestRoot.equalsIgnoreCase(targetRoot)) {
73-
foundProgramUrl = programUrl;
74-
break;
75-
}
76-
}
77-
} catch (Exception e) {
78-
// Ignore malformed URIs inside the JSON target list
79-
continue;
80-
}
81-
}
82-
83-
// Stop searching if we already found a match
84-
if (foundProgramUrl != null) {
85-
break;
86-
}
27+
java.util.List<String> programUrls = new java.util.ArrayList<>();
28+
while (urlMatcher.find()) {
29+
programUrls.add(urlMatcher.group(1));
8730
}
8831

8932
// 4. Output the results
90-
if (foundProgramUrl != null) {
91-
logging().logToOutput("[+] BBP MATCH: " + requestHost + " belongs to -> " + foundProgramUrl);
92-
}
33+
if (!programUrls.isEmpty()) {
34+
for (String programUrl : programUrls) {
35+
logging().logToOutput("[+] BBP MATCH: " + requestHost + " belongs to -> " + programUrl);
36+
}
37+
}

website/pkg/core/api.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,108 @@ func apiUpdatesHandler(w http.ResponseWriter, r *http.Request) {
673673
w.Write(respJSON)
674674
}
675675

676+
// --- Find API ---
677+
678+
type apiFindResponse struct {
679+
Query string `json:"query"`
680+
Programs []apiFindProgram `json:"programs"`
681+
TotalCount int `json:"total_count"`
682+
}
683+
684+
type apiFindProgram struct {
685+
Platform string `json:"platform"`
686+
Handle string `json:"handle"`
687+
URL string `json:"url"`
688+
}
689+
690+
// apiFindHandler serves GET /api/v1/find — finds programs matching a domain/hostname query.
691+
func apiFindHandler(w http.ResponseWriter, r *http.Request) {
692+
if r.Method == http.MethodOptions {
693+
setCORSHeaders(w)
694+
w.WriteHeader(http.StatusNoContent)
695+
return
696+
}
697+
if r.Method != http.MethodGet {
698+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
699+
return
700+
}
701+
702+
q := strings.TrimSpace(r.URL.Query().Get("q"))
703+
if q == "" {
704+
setCORSHeaders(w)
705+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
706+
w.WriteHeader(http.StatusBadRequest)
707+
w.Write([]byte(`{"error":"missing required query parameter: q"}`))
708+
return
709+
}
710+
711+
ctx := context.Background()
712+
713+
// Direct search: find programs with targets matching the literal query
714+
matches, err := db.FindProgramsByDomain(ctx, q)
715+
if err != nil {
716+
log.Printf("API find: error searching for %q: %v", q, err)
717+
setCORSHeaders(w)
718+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
719+
w.WriteHeader(http.StatusInternalServerError)
720+
w.Write([]byte(`{"error":"internal server error"}`))
721+
return
722+
}
723+
724+
// Root domain expansion: if the root domain differs from the query and is not blacklisted,
725+
// do a second search and merge results.
726+
rootDomain, ok := storage.ExtractRootDomain(q)
727+
if ok && rootDomain != q && !targets.IsBlacklistedSuffix(rootDomain) {
728+
rootMatches, err := db.FindProgramsByDomain(ctx, rootDomain)
729+
if err != nil {
730+
log.Printf("API find: error searching root domain %q: %v", rootDomain, err)
731+
// Non-fatal: continue with what we have
732+
} else {
733+
matches = append(matches, rootMatches...)
734+
}
735+
}
736+
737+
// Deduplicate by program URL
738+
seen := make(map[string]struct{})
739+
var programs []apiFindProgram
740+
for _, m := range matches {
741+
programURL := strings.ReplaceAll(m.URL, "api.yeswehack.com", "yeswehack.com")
742+
if _, exists := seen[programURL]; exists {
743+
continue
744+
}
745+
seen[programURL] = struct{}{}
746+
programs = append(programs, apiFindProgram{
747+
Platform: m.Platform,
748+
Handle: m.Handle,
749+
URL: programURL,
750+
})
751+
}
752+
753+
if programs == nil {
754+
programs = []apiFindProgram{}
755+
}
756+
757+
resp := apiFindResponse{
758+
Query: q,
759+
Programs: programs,
760+
TotalCount: len(programs),
761+
}
762+
763+
respJSON, err := json.Marshal(resp)
764+
if err != nil {
765+
log.Printf("API find: error marshaling response: %v", err)
766+
setCORSHeaders(w)
767+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
768+
w.WriteHeader(http.StatusInternalServerError)
769+
w.Write([]byte(`{"error":"internal server error"}`))
770+
return
771+
}
772+
773+
setCORSHeaders(w)
774+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
775+
w.Write(respJSON)
776+
}
777+
676778
// mergeUniqueSorted merges two sorted string slices into a single sorted, deduplicated slice.
677779
func mergeUniqueSorted(a, b []string) []string {
678780
seen := make(map[string]struct{}, len(a)+len(b))

website/pkg/core/apipage.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func APIPageContent() g.Node {
124124
Section(Class("bg-zinc-900/30 border border-zinc-800/50 rounded-2xl shadow-xl shadow-black/10 p-6 md:p-8 mb-6"),
125125
H2(Class("text-lg font-semibold text-white mb-4"), g.Text("Usage Examples")),
126126
Div(Class("bg-zinc-950 rounded-lg p-4 font-mono text-sm text-cyan-400 overflow-x-auto"),
127-
g.Raw(`<span class="text-zinc-500"># Get all in-scope wildcard domains</span><br>curl -s https://bbscope.com/api/v1/targets/wildcards<br><br><span class="text-zinc-500"># Pipe directly into your tools</span><br>curl -s https://bbscope.com/api/v1/targets/wildcards | subfinder -silent<br><br><span class="text-zinc-500"># Filter by platform and get JSON</span><br>curl -s "https://bbscope.com/api/v1/targets/domains?platform=h1&amp;format=json"<br><br><span class="text-zinc-500"># Raw data without AI enhancements</span><br>curl -s "https://bbscope.com/api/v1/targets/wildcards?raw=true"<br><br><span class="text-zinc-500"># Get scope updates (since: today, yesterday, 7d, 30d, 90d, 1y, or YYYY-MM-DD)</span><br>curl -s "https://bbscope.com/api/v1/updates?since=7d"<br><br><span class="text-zinc-500"># Filter updates by platform and date range</span><br>curl -s "https://bbscope.com/api/v1/updates?since=2025-01-01&amp;until=2025-01-31&amp;platform=h1"<br><br><span class="text-zinc-500"># Search updates and paginate</span><br>curl -s "https://bbscope.com/api/v1/updates?search=example.com&amp;per_page=50&amp;page=2"`),
127+
g.Raw(`<span class="text-zinc-500"># Get all in-scope wildcard domains</span><br>curl -s https://bbscope.com/api/v1/targets/wildcards<br><br><span class="text-zinc-500"># Pipe directly into your tools</span><br>curl -s https://bbscope.com/api/v1/targets/wildcards | subfinder -silent<br><br><span class="text-zinc-500"># Filter by platform and get JSON</span><br>curl -s "https://bbscope.com/api/v1/targets/domains?platform=h1&amp;format=json"<br><br><span class="text-zinc-500"># Raw data without AI enhancements</span><br>curl -s "https://bbscope.com/api/v1/targets/wildcards?raw=true"<br><br><span class="text-zinc-500"># Get scope updates (since: today, yesterday, 7d, 30d, 90d, 1y, or YYYY-MM-DD)</span><br>curl -s "https://bbscope.com/api/v1/updates?since=7d"<br><br><span class="text-zinc-500"># Filter updates by platform and date range</span><br>curl -s "https://bbscope.com/api/v1/updates?since=2025-01-01&amp;until=2025-01-31&amp;platform=h1"<br><br><span class="text-zinc-500"># Search updates and paginate</span><br>curl -s "https://bbscope.com/api/v1/updates?search=example.com&amp;per_page=50&amp;page=2"<br><br><span class="text-zinc-500"># Find programs that have example.com in scope</span><br>curl -s "https://bbscope.com/api/v1/find?q=example.com"<br><br><span class="text-zinc-500"># Find programs via root domain (matches *.example.com scopes)</span><br>curl -s "https://bbscope.com/api/v1/find?q=sub.example.com"`),
128128
),
129129
),
130130

@@ -169,6 +169,16 @@ func APIPageContent() g.Node {
169169
},
170170
),
171171

172+
H3(Class("text-sm font-semibold text-zinc-300 uppercase tracking-wider mb-4 mt-6"), g.Text("Find")),
173+
174+
apiEndpointCard(
175+
"GET", "/api/v1/find",
176+
"Find programs whose scope matches a given hostname or domain. Automatically expands to root domain matching (e.g. aaa.example.com matches programs scoping bbb.example.com). Cloud provider domains are excluded from expansion to avoid false positives.",
177+
[]apiParam{
178+
{"q", "string", "Search query (hostname, domain, etc.) — required"},
179+
},
180+
),
181+
172182
H3(Class("text-sm font-semibold text-zinc-300 uppercase tracking-wider mb-4 mt-6"), g.Text("Updates")),
173183

174184
apiEndpointCard(

website/pkg/core/core.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ func Run(cfg ServerConfig) error {
867867
http.HandleFunc("/api/v1/programs/", apiProgramDetailHandler)
868868
http.HandleFunc("/api/v1/targets/", apiTargetsHandler)
869869
http.HandleFunc("/api/v1/updates", apiUpdatesHandler)
870+
http.HandleFunc("/api/v1/find", apiFindHandler)
870871
http.HandleFunc("/api", apiPageHandler)
871872

872873
// Serve static files

0 commit comments

Comments
 (0)