11"""GitHub client construction and reliable code-search execution."""
22
3- import csv
43import os
54import time
65from contextlib import nullcontext
1110import github3 as github
1211from requests import exceptions as requests_exceptions
1312
13+ from github_dorks .output import create_writer
14+
1415
1516@dataclass
1617class ScanStats :
@@ -128,29 +129,19 @@ def iter_search_results(client, query, stats, max_retries=3,
128129 sleep (delay )
129130
130131
131- def _write_result (result , query , csv_writer , stdout ):
132- values = {
132+ def _result_record (result , query ):
133+ return {
133134 'dork' : query ,
134135 'text_matches' : result .text_matches ,
135136 'path' : result .path ,
136137 'score' : result .score ,
137138 'url' : result .html_url ,
138139 }
139- if csv_writer :
140- csv_writer .writerow (values .values ())
141- return
142- stdout .write ('\n ' .join ([
143- 'Found result for {dork}' ,
144- 'Text matches: {text_matches}' ,
145- 'File path: {path}' ,
146- 'Score/Relevance: {score}' ,
147- 'URL of File: {url}' ,
148- '' ,
149- ]).format (** values ) + '\n ' )
150140
151141
152142def search (repo_to_search = None , user_to_search = None , gh_dorks_file = None ,
153- output_filename = None , client = None , max_retries = 3 ,
143+ output_filename = None , output_format = None , force = False ,
144+ quiet = False , verbose = False , client = None , max_retries = 3 ,
154145 sleep = time .sleep , now = time .time , monotonic = time .monotonic ,
155146 stdout = None , stderr = None ):
156147 """Run every dork and return statistics, isolating per-query failures."""
@@ -162,50 +153,63 @@ def search(repo_to_search=None, user_to_search=None, gh_dorks_file=None,
162153 raise ValueError ('exactly one repository or user scope is required' )
163154 client = create_client () if client is None else client
164155 dorks_path = find_dorks_file (gh_dorks_file )
156+ output_format = output_format or ('csv' if output_filename else 'text' )
165157 stats = ScanStats ()
166158 started_at = monotonic ()
167159 scope = f' repo:{ repo_to_search } ' if repo_to_search else f' user:{ user_to_search } '
168160 label = 'Repo' if repo_to_search else 'User'
169- stdout .write (f'Scanning { label } : { scope .split (":" , 1 )[1 ]} \n ' )
161+ status_stream = stderr if not output_filename and output_format != 'text' else stdout
162+ if not quiet :
163+ status_stream .write (f'Scanning { label } : { scope .split (":" , 1 )[1 ]} \n ' )
170164
171165 output_context = (
172- open (output_filename , 'w' , newline = '' , encoding = 'utf-8' )
173- if output_filename else nullcontext (None )
166+ open (
167+ output_filename , 'w' if force else 'x' , newline = '' , encoding = 'utf-8'
168+ ) if output_filename else nullcontext (stdout )
174169 )
175170 with dorks_path .open (encoding = 'utf-8' ) as dork_file , output_context as output_file :
176- csv_writer = csv .writer (output_file ) if output_file else None
177- if csv_writer :
178- csv_writer .writerow ([
179- 'Issue Type (Dork)' , 'Text Matches' , 'File Path' ,
180- 'Score/Relevance' , 'URL of File' ,
181- ])
182- for line in dork_file :
183- dork = line .strip ()
184- if not dork or dork [0 ] in '#;' :
185- continue
186- query = dork + scope
187- stats .queries += 1
188- try :
189- for result in iter_search_results (
171+ writer = create_writer (output_format , output_file )
172+ writer .start ()
173+ try :
174+ for line in dork_file :
175+ dork = line .strip ()
176+ if not dork or dork [0 ] in '#;' :
177+ continue
178+ query = dork + scope
179+ stats .queries += 1
180+ if verbose and not quiet :
181+ status_stream .write (f'Searching: { query } \n ' )
182+ results = iter_search_results (
190183 client , query , stats , max_retries , sleep , now , stderr
191- ):
192- stats .matches += 1
193- _write_result (result , query , csv_writer , stdout )
194- except Exception as error :
195- authentication_error = getattr (
196- github .exceptions , 'AuthenticationFailed' , ()
197184 )
198- if authentication_error and isinstance (error , authentication_error ):
199- raise
200- stats .failures += 1
201- stderr .write (f'Query failed: { query } \n { error } \n ' )
202-
203- stats .elapsed_seconds = monotonic () - started_at
204- if not stats .matches and not stats .failures :
205- stdout .write (f'No results for your dork search{ scope } . Hurray!\n ' )
206- stdout .write (
207- f'Summary: { stats .queries } queries, { stats .matches } matches, '
208- f'{ stats .failures } failures, { stats .retries } retries, '
209- f'{ stats .elapsed_seconds :.1f} s elapsed\n '
210- )
185+ while True :
186+ try :
187+ result = next (results )
188+ except StopIteration :
189+ break
190+ except Exception as error :
191+ authentication_error = getattr (
192+ github .exceptions , 'AuthenticationFailed' , ()
193+ )
194+ if authentication_error and isinstance (
195+ error , authentication_error
196+ ):
197+ raise
198+ stats .failures += 1
199+ stderr .write (f'Query failed: { query } \n { error } \n ' )
200+ break
201+ stats .matches += 1
202+ writer .write (_result_record (result , query ))
203+ finally :
204+ stats .elapsed_seconds = monotonic () - started_at
205+ writer .finish (stats )
206+
207+ if not quiet :
208+ if not stats .matches and not stats .failures :
209+ status_stream .write (f'No results for your dork search{ scope } . Hurray!\n ' )
210+ status_stream .write (
211+ f'Summary: { stats .queries } queries, { stats .matches } matches, '
212+ f'{ stats .failures } failures, { stats .retries } retries, '
213+ f'{ stats .elapsed_seconds :.1f} s elapsed\n '
214+ )
211215 return stats
0 commit comments