Skip to content

Commit d3d0803

Browse files
authored
Merge pull request #86 from R3dTr4p/feature/bulk-download-reports
Add docs for bulk download reports and fix pagination bug
2 parents d7a4522 + c403c9f commit d3d0803

4 files changed

Lines changed: 114 additions & 9 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Visit [bbscope.com](https://bbscope.com/) to explore an hourly-updated list of p
3636
- **Track Changes**: Monitor scope additions and removals over time.
3737
- **LLM Cleanup (opt-in)**: Let GPT-style models fix messy scope strings in bulk when polling.
3838
- **Flexible Output**: Get your data in plain text, JSON, or CSV.
39+
- **Report Downloads**: Bulk download your HackerOne reports as Markdown files, with parallel fetching and filtering by program, state, or severity.
3940

4041
---
4142

@@ -288,6 +289,25 @@ Add a custom target to the database manually.
288289

289290
---
290291

292+
### `reports` - Downloading Reports
293+
294+
The `reports` command bulk downloads your vulnerability reports as Markdown files, organized by program.
295+
296+
```bash
297+
# Download all your HackerOne reports
298+
bbscope reports h1 --output-dir ./reports
299+
300+
# Preview what would be downloaded
301+
bbscope reports h1 --output-dir ./reports --dry-run
302+
303+
# Filter by program, state, or severity
304+
bbscope reports h1 --output-dir ./reports --program google --state resolved --severity critical
305+
```
306+
307+
Reports are saved as `{output-dir}/h1/{program}/{id}_{title}.md` with metadata tables and full vulnerability details. 10 parallel workers handle the downloads with automatic rate-limit handling.
308+
309+
---
310+
291311
## 📖 Examples
292312

293313
**1. First-Time Setup: Poll all private, bounty-only programs and save to DB**

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [Polling Scopes](./cli/polling.md)
1414
- [Database Commands](./cli/database.md)
1515
- [Extracting Targets](./cli/targets.md)
16+
- [Downloading Reports](./cli/reports.md)
1617
- [Output Formatting](./cli/output.md)
1718

1819
# Database

docs/src/cli/reports.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Downloading Reports
2+
3+
The `bbscope reports` command downloads your vulnerability reports from bug bounty platforms as Markdown files.
4+
5+
## HackerOne
6+
7+
```bash
8+
# Download all your reports
9+
bbscope reports h1 --output-dir ./reports
10+
11+
# Preview what would be downloaded (dry-run)
12+
bbscope reports h1 --output-dir ./reports --dry-run
13+
14+
# Filter by program
15+
bbscope reports h1 --output-dir ./reports --program google --program microsoft
16+
17+
# Filter by state (e.g., resolved, triaged, new, duplicate, informative, not-applicable, spam)
18+
bbscope reports h1 --output-dir ./reports --state resolved --state triaged
19+
20+
# Filter by severity
21+
bbscope reports h1 --output-dir ./reports --severity critical --severity high
22+
23+
# Combine filters
24+
bbscope reports h1 --output-dir ./reports --program google --state resolved --severity critical
25+
26+
# Overwrite existing files
27+
bbscope reports h1 --output-dir ./reports --overwrite
28+
```
29+
30+
### Authentication
31+
32+
Credentials can be provided via CLI flags or config file:
33+
34+
```bash
35+
# CLI flags
36+
bbscope reports h1 --user your_username --token your_api_token --output-dir ./reports
37+
```
38+
39+
```yaml
40+
# ~/.bbscope.yaml
41+
hackerone:
42+
username: "your_username"
43+
token: "your_api_token"
44+
```
45+
46+
### Output structure
47+
48+
Reports are saved as Markdown files organized by program:
49+
50+
```
51+
reports/
52+
└── h1/
53+
├── google/
54+
│ ├── 123456_XSS_in_login_page.md
55+
│ └── 123457_IDOR_in_user_profile.md
56+
└── microsoft/
57+
└── 234567_SSRF_in_webhook_handler.md
58+
```
59+
60+
Each file contains a metadata table (ID, program, state, severity, weakness, asset, bounty, CVE IDs, timestamps) followed by the vulnerability information and impact sections.
61+
62+
### Dry-run output
63+
64+
The `--dry-run` flag prints a table of matching reports without downloading:
65+
66+
```
67+
ID PROGRAM STATE SEVERITY CREATED TITLE
68+
123456 google resolved high 2024-01-15T10:30:00.000Z XSS in login page
69+
123457 google triaged critical 2024-02-20T14:00:00.000Z IDOR in user profile
70+
```
71+
72+
## Flags
73+
74+
| Flag | Short | Description |
75+
|------|-------|-------------|
76+
| `--output-dir` | | Output directory for downloaded reports (required) |
77+
| `--program` | | Filter by program handle(s) |
78+
| `--state` | | Filter by report state(s) |
79+
| `--severity` | | Filter by severity level(s) |
80+
| `--dry-run` | | List reports without downloading |
81+
| `--overwrite` | | Overwrite existing report files |
82+
83+
## How it works
84+
85+
1. **List phase**: fetches all report summaries from the HackerOne API (`/v1/hackers/me/reports`), paginated at 100 per page. Filters are applied server-side using Lucene query syntax.
86+
2. **Download phase**: 10 parallel workers fetch full report details (`/v1/hackers/reports/{id}`) and write them as Markdown files.
87+
3. **Skip logic**: existing files are skipped unless `--overwrite` is set.
88+
4. **Rate limiting**: HTTP 429 responses trigger a 60-second backoff. Other transient errors are retried up to 3 times with a 2-second delay.
89+
90+
> **Note**: The HackerOne Hacker API may not return draft reports or reports where you are a collaborator but not the primary reporter. If your downloaded count is lower than your dashboard total, this is likely the cause.

pkg/reports/hackerone.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ func (f *H1Fetcher) ListReports(ctx context.Context, opts FetchOptions) ([]Repor
3737
for {
3838
body, err := f.doRequest(currentURL)
3939
if err != nil {
40+
utils.Log.Warnf("Error fetching report list page: %v", err)
4041
return summaries, err
4142
}
42-
if body == "" {
43-
break // non-retryable status, stop
44-
}
4543

4644
count := int(gjson.Get(body, "data.#").Int())
4745
for i := 0; i < count; i++ {
@@ -77,10 +75,7 @@ func (f *H1Fetcher) FetchReport(ctx context.Context, reportID string) (*Report,
7775

7876
body, err := f.doRequest(url)
7977
if err != nil {
80-
return nil, err
81-
}
82-
if body == "" {
83-
return nil, fmt.Errorf("report %s: not found or not accessible", reportID)
78+
return nil, fmt.Errorf("report %s: %w", reportID, err)
8479
}
8580

8681
r := &Report{
@@ -155,8 +150,7 @@ func (f *H1Fetcher) doRequest(url string) (string, error) {
155150

156151
// Non-retryable errors
157152
if res.StatusCode == 400 || res.StatusCode == 403 || res.StatusCode == 404 {
158-
utils.Log.Warnf("Got status %d for %s, skipping", res.StatusCode, url)
159-
return "", nil
153+
return "", fmt.Errorf("got status %d for %s", res.StatusCode, url)
160154
}
161155

162156
if res.StatusCode != 200 {

0 commit comments

Comments
 (0)