|
1 | | -import github3 as github |
2 | | -import os |
| 1 | +"""Command-line interface for github-dorks.""" |
| 2 | + |
3 | 3 | import argparse |
4 | | -import csv |
| 4 | +import os |
| 5 | +import sys |
5 | 6 | import time |
| 7 | + |
6 | 8 | import feedparser |
7 | | -from copy import copy |
8 | | -from contextlib import nullcontext |
9 | | -from sys import stderr, prefix |
| 9 | +import github3 as github |
10 | 10 |
|
11 | 11 | from github_dorks import __version__ |
| 12 | +from github_dorks.search import create_client, search |
12 | 13 |
|
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) |
17 | 14 |
|
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') |
23 | 19 |
|
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}' |
26 | 23 | 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(): |
169 | 37 | parser = argparse.ArgumentParser( |
170 | 38 | prog='github-dorks', |
171 | 39 | 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 | + ) |
174 | 42 | parser.add_argument( |
175 | | - '-v', '--version', action='version', version='%(prog)s ' + __version__) |
176 | | - |
| 43 | + '-v', '--version', action='version', version='%(prog)s ' + __version__ |
| 44 | + ) |
177 | 45 | group = parser.add_mutually_exclusive_group(required=True) |
178 | 46 | 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 | + ) |
185 | 50 | 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 | + ) |
199 | 54 | 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', |
205 | 57 | ) |
206 | | - |
207 | 58 | 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', |
213 | 61 | ) |
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 |
222 | 71 |
|
223 | 72 |
|
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