Skip to content

Commit 6163fbe

Browse files
authored
Merge pull request #68 from techgaun/feat/search-reliability
feat: make GitHub searches resilient
2 parents 263ba53 + 5a3807a commit 6163fbe

7 files changed

Lines changed: 491 additions & 218 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,18 @@ GH_USER=techgaun GH_PWD=<mypass> github-dorks -u dev-nepal # search as authe
6060
GH_TOKEN=<github_token> github-dorks -u dev-nepal # search using auth token
6161

6262
GH_URL=https://github.example.com github-dorks -u dev-nepal # search a GitHub Enterprise instance
63+
64+
github-dorks -r techgaun/github-dorks --max-retries 5 # retry recoverable failures up to five times
6365
```
6466

6567
The legacy `python github-dork.py ...` invocation remains available for
6668
compatibility. The package can also run as `python -m github_dorks ...`.
6769

70+
Each scan ends with a summary of queries, matches, failures, retries, and
71+
elapsed time. The command exits with status `0` after a complete scan, `2` when
72+
one or more queries failed, and `1` for fatal configuration, file, or
73+
authentication errors.
74+
6875
### Development
6976

7077
Run the dependency-free unit test suite with:
@@ -77,9 +84,9 @@ The CI test matrix covers Python 3.10 through 3.13.
7784

7885
### Limitations
7986

80-
- Authenticated requests get a higher rate limit. But, since this tool waits for the api rate limit to be reset (which is usually less than a minute), it can be slightly slow.
87+
- Authenticated requests receive higher rate limits. Searches may pause until
88+
GitHub resets the search limit.
8189
- Search results can be printed to the terminal or written as CSV.
82-
- ~~Handle rate limit and retry. PR welcome~~
8390

8491
### Contribution
8592

‎github-dork.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66

77
if __name__ == '__main__':
8-
main()
8+
raise SystemExit(main())

‎github_dorks/__main__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33

44
if __name__ == '__main__':
5-
main()
5+
raise SystemExit(main())

‎github_dorks/cli.py‎

Lines changed: 79 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,225 +1,101 @@
1-
import github3 as github
2-
import os
1+
"""Command-line interface for github-dorks."""
2+
33
import argparse
4-
import csv
4+
import os
5+
import sys
56
import time
7+
68
import feedparser
7-
from copy import copy
8-
from contextlib import nullcontext
9-
from sys import stderr, prefix
9+
import github3 as github
1010

1111
from github_dorks import __version__
12+
from github_dorks.search import create_client, search
1213

13-
gh_user = os.getenv('GH_USER', None)
14-
gh_pass = os.getenv('GH_PWD', None)
15-
gh_token = os.getenv('GH_TOKEN', None)
16-
gh_url = os.getenv('GH_URL', None)
1714

18-
if gh_url is None:
19-
gh = github.GitHub(username=gh_user, password=gh_pass, token=gh_token)
20-
else:
21-
gh = github.GitHubEnterprise(
22-
url=gh_url, username=gh_user, password=gh_pass, token=gh_token)
15+
def monitor(gh_dorks_file=None, feed_token=None, refresh_time=60):
16+
github_user = os.getenv('GH_USER')
17+
if github_user is None:
18+
raise ValueError('GH_USER is required for monitoring')
2319

24-
25-
def search_wrapper(gen):
20+
print('Monitoring merged pull requests for new scans.')
21+
seen_items = set()
22+
private_feed = f'https://github.com/{github_user}.private.atom?token={feed_token}'
2623
while True:
27-
gen_back = copy(gen)
28-
try:
29-
yield next(gen)
30-
except StopIteration:
31-
return
32-
except github.exceptions.ForbiddenError:
33-
search_rate_limit = gh.rate_limit()['resources']['search']
34-
# limit_remaining = search_rate_limit['remaining']
35-
reset_time = search_rate_limit['reset']
36-
current_time = int(time.time())
37-
sleep_time = reset_time - current_time + 1
38-
stderr.write(
39-
'GitHub Search API rate limit reached. Sleeping for %d seconds.\n\n'
40-
% (sleep_time))
41-
time.sleep(sleep_time)
42-
yield next(gen_back)
43-
except Exception as e:
44-
raise e
45-
46-
47-
def metasearch(repo_to_search=None,
48-
user_to_search=None,
49-
gh_dorks_file=None,
50-
active_monit=None,
51-
output_filename=None,
52-
refresh_time=60):
53-
if active_monit is None:
54-
search(repo_to_search, user_to_search, gh_dorks_file, active_monit, output_filename)
55-
else:
56-
monit(gh_dorks_file, active_monit, refresh_time)
57-
58-
59-
def monit(gh_dorks_file=None, active_monit=None, refresh_time=60):
60-
if gh_user is None:
61-
raise Exception('Error, env Github user variable needed')
62-
else:
63-
print(
64-
'Monitoring user private feed searching new code to be dorked.' +
65-
'Every new merged pull request trigger user scan.'
66-
)
67-
print('-----')
68-
items_history = list()
69-
gh_private_feed = "https://github.com/{}.private.atom?token={}".format(
70-
gh_user, active_monit)
71-
while True:
72-
feed = feedparser.parse(gh_private_feed)
73-
for i in feed['items']:
74-
if 'merged pull' in i['title']:
75-
if i['title'] not in items_history:
76-
search(
77-
user_to_search=i['author_detail']['name'],
78-
gh_dorks_file=gh_dorks_file)
79-
items_history.append(i['title'])
80-
print('Waiting for new items...')
81-
time.sleep(refresh_time)
82-
83-
84-
def search(repo_to_search=None,
85-
user_to_search=None,
86-
gh_dorks_file=None,
87-
active_monit=None,
88-
output_filename=None):
89-
90-
if gh_dorks_file is None:
91-
for path_prefix in ['.', os.path.join(prefix, 'github-dorks/')]:
92-
filename = os.path.join(path_prefix, 'github-dorks.txt')
93-
if os.path.isfile(filename):
94-
gh_dorks_file = filename
95-
break
96-
97-
if gh_dorks_file is None or not os.path.isfile(gh_dorks_file):
98-
raise Exception('Error, the dorks file path is not valid')
99-
if user_to_search:
100-
print("Scanning User: ", user_to_search)
101-
if repo_to_search:
102-
print("Scanning Repo: ", repo_to_search)
103-
found = False
104-
105-
output_context = (
106-
open(output_filename, 'w', newline='', encoding='utf-8')
107-
if output_filename else nullcontext(None)
108-
)
109-
110-
with open(gh_dorks_file, 'r', encoding='utf-8') as dork_file, output_context as output_file:
111-
# Write CSV Header
112-
csv_writer = None
113-
if output_file:
114-
csv_writer = csv.writer(output_file)
115-
csv_writer.writerow([
116-
'Issue Type (Dork)', 'Text Matches', 'File Path',
117-
'Score/Relevance', 'URL of File'
118-
])
119-
for dork in dork_file:
120-
dork = dork.strip()
121-
if not dork or dork[0] in '#;':
122-
continue
123-
addendum = ''
124-
if repo_to_search:
125-
addendum = ' repo:' + repo_to_search
126-
elif user_to_search:
127-
addendum = ' user:' + user_to_search
128-
129-
dork = dork + addendum
130-
search_results = search_wrapper(gh.search_code(dork))
131-
try:
132-
for search_result in search_results:
133-
found = True
134-
fmt_args = {
135-
'dork': dork,
136-
'text_matches': search_result.text_matches,
137-
'path': search_result.path,
138-
'score': search_result.score,
139-
'url': search_result.html_url
140-
}
141-
142-
# Either write to file or print output
143-
if csv_writer:
144-
csv_writer.writerow([
145-
fmt_args['dork'], fmt_args['text_matches'],
146-
fmt_args['path'], fmt_args['score'], fmt_args['url']
147-
])
148-
else:
149-
result = '\n'.join([
150-
'Found result for {dork}',
151-
'Text matches: {text_matches}', 'File path: {path}',
152-
'Score/Relevance: {score}', 'URL of File: {url}', ''
153-
]).format(**fmt_args)
154-
print(result)
155-
156-
except github.exceptions.GitHubError as e:
157-
print('GitHubError encountered on search of dork: ' + dork)
158-
print(e)
159-
return
160-
except Exception as e:
161-
print(e)
162-
print('Error encountered on search of dork: ' + dork)
163-
164-
if not found:
165-
print('No results for your dork search' + addendum + '. Hurray!')
166-
167-
168-
def main():
24+
feed = feedparser.parse(private_feed)
25+
for item in feed['items']:
26+
if 'merged pull' in item['title'] and item['title'] not in seen_items:
27+
search(
28+
user_to_search=item['author_detail']['name'],
29+
gh_dorks_file=gh_dorks_file,
30+
)
31+
seen_items.add(item['title'])
32+
print('Waiting for new items...')
33+
time.sleep(refresh_time)
34+
35+
36+
def build_parser():
16937
parser = argparse.ArgumentParser(
17038
prog='github-dorks',
17139
description='Search GitHub for sensitive data patterns',
172-
epilog='Use responsibly. Only scan repositories you are authorized to assess.')
173-
40+
epilog='Use responsibly. Only scan repositories you are authorized to assess.',
41+
)
17442
parser.add_argument(
175-
'-v', '--version', action='version', version='%(prog)s ' + __version__)
176-
43+
'-v', '--version', action='version', version='%(prog)s ' + __version__
44+
)
17745
group = parser.add_mutually_exclusive_group(required=True)
17846
group.add_argument(
179-
'-u',
180-
'--user',
181-
dest='user_to_search',
182-
action='store',
183-
help='GitHub user/org to search within. Eg: techgaun')
184-
47+
'-u', '--user', dest='user_to_search',
48+
help='GitHub user/org to search within. Eg: techgaun',
49+
)
18550
group.add_argument(
186-
'-r',
187-
'--repo',
188-
dest='repo_to_search',
189-
action='store',
190-
help='GitHub repo to search within. Eg: techgaun/github-dorks')
191-
192-
parser.add_argument(
193-
'-d',
194-
'--dork',
195-
dest='gh_dorks_file',
196-
action='store',
197-
help='GitHub dorks file. Eg: github-dorks.txt')
198-
51+
'-r', '--repo', dest='repo_to_search',
52+
help='GitHub repo to search within. Eg: techgaun/github-dorks',
53+
)
19954
group.add_argument(
200-
'-m',
201-
'--monit',
202-
dest='active_monit',
203-
action='store',
204-
help='Monitors GitHub user private feed with feed token'
55+
'-m', '--monit', dest='active_monit',
56+
help='Monitor the GitHub user private feed with this feed token',
20557
)
206-
20758
parser.add_argument(
208-
'-o',
209-
'--outputFile',
210-
dest='output_filename',
211-
action='store',
212-
help='CSV File to write results to. This overwrites the file provided! Eg: out.csv'
59+
'-d', '--dork', dest='gh_dorks_file',
60+
help='GitHub dorks file. Eg: github-dorks.txt',
21361
)
214-
215-
args = parser.parse_args()
216-
metasearch(
217-
repo_to_search=args.repo_to_search,
218-
user_to_search=args.user_to_search,
219-
gh_dorks_file=args.gh_dorks_file,
220-
active_monit=args.active_monit,
221-
output_filename=args.output_filename)
62+
parser.add_argument(
63+
'-o', '--outputFile', dest='output_filename',
64+
help='CSV file to write results to. This overwrites the provided file.',
65+
)
66+
parser.add_argument(
67+
'--max-retries', type=int, default=3,
68+
help='Maximum retries per query for recoverable API failures (default: 3)',
69+
)
70+
return parser
22271

22372

224-
if __name__ == '__main__':
225-
main()
73+
def main():
74+
parser = build_parser()
75+
args = parser.parse_args()
76+
if args.max_retries < 0:
77+
parser.error('--max-retries must be zero or greater')
78+
try:
79+
if args.active_monit:
80+
monitor(args.gh_dorks_file, args.active_monit)
81+
return 0
82+
stats = search(
83+
repo_to_search=args.repo_to_search,
84+
user_to_search=args.user_to_search,
85+
gh_dorks_file=args.gh_dorks_file,
86+
output_filename=args.output_filename,
87+
client=create_client(),
88+
max_retries=args.max_retries,
89+
)
90+
return stats.exit_code
91+
except (OSError, ValueError) as error:
92+
print(f'Error: {error}', file=sys.stderr)
93+
return 1
94+
except Exception as error:
95+
authentication_error = getattr(
96+
github.exceptions, 'AuthenticationFailed', ()
97+
)
98+
if authentication_error and isinstance(error, authentication_error):
99+
print(f'Error: {error}', file=sys.stderr)
100+
return 1
101+
raise

0 commit comments

Comments
 (0)