@@ -8,6 +8,18 @@ use crate::fuzz::FuzzEngine;
8
8
use crate :: plugin:: { PluginAction , PluginManager } ;
9
9
use crate :: report:: { ReportFormat , ReportGenerator } ;
10
10
11
+ #[ derive( Debug ) ]
12
+ pub struct ScanConfig {
13
+ pub path : PathBuf ,
14
+ pub config : Option < PathBuf > ,
15
+ pub output : PathBuf ,
16
+ pub formats : Vec < ReportFormat > ,
17
+ pub json_only : bool ,
18
+ pub html_only : bool ,
19
+ pub no_open : bool ,
20
+ pub fail_on_critical : bool ,
21
+ }
22
+
11
23
#[ derive( Parser ) ]
12
24
#[ command( name = "solsec" ) ]
13
25
#[ command( about = "Solana Smart Contract Security Toolkit" ) ]
@@ -46,6 +58,10 @@ pub enum Commands {
46
58
#[ arg( long, conflicts_with = "format" ) ]
47
59
html_only : bool ,
48
60
61
+ /// Don't automatically open HTML report in browser (opens by default in interactive mode)
62
+ #[ arg( long) ]
63
+ no_open : bool ,
64
+
49
65
/// Fail with non-zero exit code on critical issues
50
66
#[ arg( long, default_value = "true" ) ]
51
67
fail_on_critical : bool ,
@@ -81,29 +97,28 @@ pub enum Commands {
81
97
} ,
82
98
}
83
99
84
- pub async fn handle_scan_command (
85
- path : PathBuf ,
86
- config : Option < PathBuf > ,
87
- output : PathBuf ,
88
- formats : Vec < ReportFormat > ,
89
- json_only : bool ,
90
- html_only : bool ,
91
- fail_on_critical : bool ,
92
- ) -> Result < ( ) > {
93
- info ! ( "Starting static analysis scan on: {}" , path. display( ) ) ;
100
+ pub async fn handle_scan_command ( config : ScanConfig ) -> Result < ( ) > {
101
+ info ! (
102
+ "Starting static analysis scan on: {}" ,
103
+ config. path. display( )
104
+ ) ;
94
105
95
- let mut analyzer = StaticAnalyzer :: new ( config) ?;
96
- let results = analyzer. analyze_path ( & path) . await ?;
106
+ let mut analyzer = StaticAnalyzer :: new ( config. config ) ?;
107
+ let results = analyzer. analyze_path ( & config . path ) . await ?;
97
108
98
109
// Determine which formats to generate
99
- let formats_to_generate = if json_only {
110
+ let formats_to_generate = if config . json_only {
100
111
vec ! [ ReportFormat :: Json ]
101
- } else if html_only {
112
+ } else if config . html_only {
102
113
vec ! [ ReportFormat :: Html ]
103
114
} else {
104
- formats
115
+ config . formats
105
116
} ;
106
117
118
+ // Check if we should open HTML before generating reports
119
+ let should_open = should_open_html ( & formats_to_generate, config. no_open ) ;
120
+ let mut html_file_path: Option < PathBuf > = None ;
121
+
107
122
// Generate reports in all requested formats
108
123
let report_gen = ReportGenerator :: new ( ) ;
109
124
for format in formats_to_generate {
@@ -114,14 +129,19 @@ pub async fn handle_scan_command(
114
129
ReportFormat :: Csv => "csv" ,
115
130
} ;
116
131
117
- let output_file = if output. extension ( ) . is_some ( ) {
132
+ let output_file = if config . output . extension ( ) . is_some ( ) {
118
133
// If user provided a specific filename, respect it for the first format
119
- output. clone ( )
134
+ config . output . clone ( )
120
135
} else {
121
136
// Generate appropriate filename based on format
122
- output. join ( format ! ( "security-report.{}" , extension) )
137
+ config . output . join ( format ! ( "security-report.{}" , extension) )
123
138
} ;
124
139
140
+ // Track HTML file path for opening later
141
+ if matches ! ( format, ReportFormat :: Html ) {
142
+ html_file_path = Some ( output_file. clone ( ) ) ;
143
+ }
144
+
125
145
report_gen
126
146
. generate_report ( & results, & output_file, format. clone ( ) )
127
147
. await ?;
@@ -135,11 +155,18 @@ pub async fn handle_scan_command(
135
155
critical_count, high_count
136
156
) ;
137
157
138
- if fail_on_critical && critical_count > 0 {
158
+ if config . fail_on_critical && critical_count > 0 {
139
159
error ! ( "Critical issues found. Failing as requested." ) ;
140
160
std:: process:: exit ( 1 ) ;
141
161
}
142
162
163
+ // Open HTML report in browser if appropriate
164
+ if should_open {
165
+ if let Some ( html_path) = html_file_path {
166
+ open_html_file ( & html_path) ?;
167
+ }
168
+ }
169
+
143
170
Ok ( ( ) )
144
171
}
145
172
@@ -199,3 +226,65 @@ pub async fn handle_plugin_command(action: PluginAction, path: Option<PathBuf>)
199
226
200
227
Ok ( ( ) )
201
228
}
229
+
230
+ /// Detects if we're running in a CI environment
231
+ fn is_ci_environment ( ) -> bool {
232
+ // Check common CI environment variables
233
+ std:: env:: var ( "CI" ) . is_ok ( )
234
+ || std:: env:: var ( "GITHUB_ACTIONS" ) . is_ok ( )
235
+ || std:: env:: var ( "GITLAB_CI" ) . is_ok ( )
236
+ || std:: env:: var ( "JENKINS_URL" ) . is_ok ( )
237
+ || std:: env:: var ( "TRAVIS" ) . is_ok ( )
238
+ || std:: env:: var ( "CIRCLECI" ) . is_ok ( )
239
+ || std:: env:: var ( "BUILDKITE" ) . is_ok ( )
240
+ || std:: env:: var ( "TF_BUILD" ) . is_ok ( ) // Azure DevOps
241
+ }
242
+
243
+ /// Detects if we're in an interactive terminal session
244
+ fn is_interactive ( ) -> bool {
245
+ // Check if stdout is a terminal and not redirected
246
+ use std:: io:: IsTerminal ;
247
+ std:: io:: stdout ( ) . is_terminal ( )
248
+ }
249
+
250
+ /// Determines if we should automatically open the HTML report
251
+ fn should_open_html ( formats : & [ ReportFormat ] , no_open : bool ) -> bool {
252
+ // Don't open if user explicitly disabled it
253
+ if no_open {
254
+ return false ;
255
+ }
256
+
257
+ // Don't open in CI environments
258
+ if is_ci_environment ( ) {
259
+ return false ;
260
+ }
261
+
262
+ // Don't open if not in interactive terminal
263
+ if !is_interactive ( ) {
264
+ return false ;
265
+ }
266
+
267
+ // Only open if HTML is being generated
268
+ formats. contains ( & ReportFormat :: Html )
269
+ }
270
+
271
+ /// Opens the HTML file in the default browser
272
+ fn open_html_file ( file_path : & PathBuf ) -> Result < ( ) > {
273
+ match opener:: open ( file_path) {
274
+ Ok ( ( ) ) => {
275
+ info ! (
276
+ "📖 Opening security report in browser: {}" ,
277
+ file_path. display( )
278
+ ) ;
279
+ Ok ( ( ) )
280
+ }
281
+ Err ( e) => {
282
+ warn ! (
283
+ "Could not open HTML report in browser: {}. You can manually open: {}" ,
284
+ e,
285
+ file_path. display( )
286
+ ) ;
287
+ Ok ( ( ) ) // Don't fail the entire command if browser opening fails
288
+ }
289
+ }
290
+ }
0 commit comments