11using System ;
2- using System . Data . SqlClient ;
2+ using Microsoft . Data . SqlClient ;
33using System . Reflection ;
44using System . Text ;
5- using System . Threading ;
5+ using System . Text . RegularExpressions ;
66using System . Threading . Tasks ;
77using Microsoft . Extensions . Logging ;
88using Spectre . Console ;
@@ -19,62 +19,96 @@ public class PingCommand : AsyncCommand<ConsoleSettings>
1919 public override async Task < int > ExecuteAsync ( CommandContext context , ConsoleSettings settings )
2020 {
2121 if ( settings . SQLCommand == null )
22- settings . SQLCommand = $ @ "SELECT @@SERVERNAME AS ""Server"", name as ""Database"", state_desc AS ""State"", replica_id AS ""Replica"" FROM sys.databases WHERE name = ' { settings . Database } ' ";
22+ settings . SQLCommand = @"SELECT @@SERVERNAME AS ""Server"", name as ""Database"", state_desc AS ""State"", replica_id AS ""Replica"" FROM sys.databases WHERE name = @DatabaseName " ;
2323
24- //Logger.LogInformation("Connection string: {Mandatory}", connectionString);
25- //Logger.LogInformation("SQL Command: {Optional}", settings.SQLCommand);
26- //Logger.LogInformation("CommandOptionFlag: {CommandOptionFlag}", settings.CommandOptionFlag);
27- //Logger.LogInformation("CommandOptionValue: {CommandOptionValue}", settings.CommandOptionValue);
24+ // Apply secure credential handling
25+ ApplySecureCredentials ( settings ) ;
2826
2927 var connString = GetConnectionString ( settings ) ;
3028
31- //Logger.LogInformation("");
32- AnsiConsole . MarkupLine ( $ "ConnectionString: [teal]{ connString . EscapeMarkup ( ) } [/]") ;
29+ AnsiConsole . MarkupLine ( $ "ConnectionString: [teal]{ RedactConnectionString ( connString ) . EscapeMarkup ( ) } [/]") ;
3330 AnsiConsole . MarkupLine ( $ "SQL Query : [teal]{ settings . SQLCommand . EscapeMarkup ( ) } [/]") ;
3431
32+ // Helpful warning if using IP
33+ if ( LooksLikeIp ( settings . Server )
34+ && ( settings . TrustServerCertificate != true )
35+ && string . IsNullOrWhiteSpace ( settings . HostNameInCertificate ) )
36+ {
37+ AnsiConsole . MarkupLine ( "[yellow]Hint:[/] You're connecting by IP. TLS certificate name validation usually fails with IPs unless the cert has the IP in SAN. Use a DNS name, set [teal]--hostname-in-certificate[/], or [teal]--trust-server-certificate true[/] for dev." ) ;
38+ }
39+
3540 bool running = true ;
3641
3742 while ( running )
3843 {
39-
4044 int sec = settings . Wait ;
41- AnsiConsole . Status ( )
45+ await AnsiConsole . Status ( )
4246 . AutoRefresh ( true )
43- . Spinner ( Spinner . Known . Dots ) // https://jsfiddle.net/sindresorhus/2eLtsbey/embedded/result/
47+ . Spinner ( Spinner . Known . Dots )
4448 . SpinnerStyle ( Style . Parse ( "green bold" ) )
45- . Start ( "Please wait..." , ctx =>
49+ . StartAsync ( "Please wait..." , async ctx =>
4650 {
47- // Simulate some work
4851 ctx . Status ( $ "Trying to connect to server [teal]{ settings . Server . EscapeMarkup ( ) } [/]...") ;
49- CallDatabase ( connString , settings ) ;
52+ await CallDatabaseAsync ( connString , settings ) ;
5053
51- // Update the status and spinner
5254 ctx . Status ( $ "Waiting [teal]{ sec } [/] seconds...") ;
5355
5456 if ( settings . NonStop )
55- Thread . Sleep ( TimeSpan . FromSeconds ( sec ) ) ;
57+ await Task . Delay ( TimeSpan . FromSeconds ( sec ) ) ;
5658 else
5759 running = false ;
5860 } ) ;
5961 }
6062
61- //Console.WriteLine("\nDone. Press enter.");
62- //Console.ReadLine();
63-
6463 return await Task . FromResult ( 0 ) ;
6564 }
6665
6766 // Validate as part of the command. This is a good way of validating options if you require any injected services.
6867 public override ValidationResult Validate ( CommandContext context , ConsoleSettings settings )
6968 {
70- //if (settings.Wait < 1)
71- // return ValidationResult.Error("...");
7269 return ValidationResult . Success ( ) ;
7370 }
7471
72+ private static void ApplySecureCredentials ( ConsoleSettings settings )
73+ {
74+ // Check for credentials from environment variables first
75+ var envUsername = Environment . GetEnvironmentVariable ( "SQLPING_USERNAME" ) ;
76+ var envPassword = Environment . GetEnvironmentVariable ( "SQLPING_PASSWORD" ) ;
77+
78+ bool passwordProvidedViaCommandLine = ! string . IsNullOrEmpty ( settings . Password ) ;
79+
80+ // Use environment variables if command line values are not provided
81+ if ( string . IsNullOrEmpty ( settings . Username ) && ! string . IsNullOrEmpty ( envUsername ) )
82+ {
83+ settings . Username = envUsername ;
84+ AnsiConsole . MarkupLine ( "[yellow]Using username from SQLPING_USERNAME environment variable[/]" ) ;
85+ }
86+
87+ if ( string . IsNullOrEmpty ( settings . Password ) && ! string . IsNullOrEmpty ( envPassword ) )
88+ {
89+ settings . Password = envPassword ;
90+ AnsiConsole . MarkupLine ( "[yellow]Using password from SQLPING_PASSWORD environment variable[/]" ) ;
91+ }
92+
93+ // If username is provided but password is not, prompt for password
94+ if ( ! string . IsNullOrEmpty ( settings . Username ) && string . IsNullOrEmpty ( settings . Password ) )
95+ {
96+ settings . Password = AnsiConsole . Prompt (
97+ new TextPrompt < string > ( "[yellow]Password:[/]" )
98+ . PromptStyle ( "red" )
99+ . Secret ( ) ) ;
100+ }
101+
102+ // Warn if password was provided via command line (security risk)
103+ if ( passwordProvidedViaCommandLine )
104+ {
105+ AnsiConsole . MarkupLine ( "[yellow]WARNING: Password provided via command line is visible in process list and shell history.[/]" ) ;
106+ AnsiConsole . MarkupLine ( "[yellow]Consider using SQLPING_PASSWORD environment variable or interactive prompt instead.[/]" ) ;
107+ }
108+ }
109+
75110 private static string GetConnectionString ( ConsoleSettings settings ) {
76111 SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder ( ) ;
77- //builder.ConnectionString = settings.ConnectionString;
78112 builder . DataSource = settings . Server ;
79113 if ( ! string . IsNullOrEmpty ( settings . Username ) ) {
80114 builder . UserID = settings . Username ;
@@ -94,44 +128,155 @@ private static string GetConnectionString(ConsoleSettings settings) {
94128 builder . WorkstationID = Environment . MachineName ;
95129 builder . ApplicationName = Assembly . GetExecutingAssembly ( ) . FullName ;
96130
131+ // TLS related options
132+ if ( settings . TrustServerCertificate . HasValue )
133+ builder . TrustServerCertificate = settings . TrustServerCertificate . Value ;
134+
135+ if ( ! string . IsNullOrWhiteSpace ( settings . Encrypt ) )
136+ {
137+ var encValue = settings . Encrypt . Trim ( ) . ToLowerInvariant ( ) ;
138+ if ( encValue is "true" or "false" or "strict" )
139+ {
140+ builder [ "Encrypt" ] = settings . Encrypt ;
141+ }
142+ else
143+ {
144+ AnsiConsole . MarkupLine ( "[yellow]Invalid --encrypt value. Use true|false|strict. Ignoring.[/]" ) ;
145+ }
146+ }
147+
148+ if ( ! string . IsNullOrWhiteSpace ( settings . HostNameInCertificate ) )
149+ {
150+ builder [ "HostNameInCertificate" ] = settings . HostNameInCertificate ;
151+ }
152+
153+ if ( settings . NoTransparentNetworkIPResolution )
154+ {
155+ builder [ "TransparentNetworkIPResolution" ] = "false" ;
156+ }
157+
97158 string connString = builder . ConnectionString ;
98159 return connString ;
99160 }
100161
101- private void CallDatabase ( string connString , ConsoleSettings settings )
162+ private static string RedactConnectionString ( string connectionString )
163+ {
164+ try
165+ {
166+ SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder ( connectionString ) ;
167+
168+ // Redact password if present
169+ if ( ! string . IsNullOrEmpty ( builder . Password ) )
170+ {
171+ builder . Password = "***REDACTED***" ;
172+ }
173+
174+ return builder . ConnectionString ;
175+ }
176+ catch
177+ {
178+ // If parsing fails, return a generic redacted message
179+ return "***CONNECTION STRING REDACTED***" ;
180+ }
181+ }
182+
183+ private async Task CallDatabaseAsync ( string connString , ConsoleSettings settings )
102184 {
103185 AnsiConsole . MarkupLine ( $ "[teal]{ DateTime . Now . ToLocalTime ( ) } [/] Connecting to { settings . Server . EscapeMarkup ( ) } ... ") ;
104186
105187 try
106188 {
107-
108- using ( SqlConnection connection = new SqlConnection ( connString ) )
189+
190+ await using ( SqlConnection connection = new SqlConnection ( connString ) )
109191 {
110- connection . Open ( ) ;
192+ await connection . OpenAsync ( ) ;
193+
194+ // Ensure we are in the requested database even if Initial Catalog was not applied for any reason
195+ if ( ! string . IsNullOrWhiteSpace ( settings . Database ) )
196+ {
197+ try
198+ {
199+ connection . ChangeDatabase ( settings . Database ) ;
200+ }
201+ catch ( Exception dbEx )
202+ {
203+ AnsiConsole . MarkupLine ( $ "[yellow]Warning:[/] Failed to change database to '[teal]{ settings . Database . EscapeMarkup ( ) } [/]': { dbEx . Message . EscapeMarkup ( ) } ") ;
204+ }
205+ }
206+
111207 var sb = new StringBuilder ( ) ;
112- using ( SqlCommand command = new SqlCommand ( settings . SQLCommand , connection ) )
208+ await using ( SqlCommand command = new SqlCommand ( settings . SQLCommand ! , connection ) )
113209 {
114- using ( SqlDataReader reader = command . ExecuteReader ( ) )
210+ // Add parameter if the query contains @DatabaseName placeholder
211+ if ( settings . SQLCommand != null && settings . SQLCommand . Contains ( "@DatabaseName" ) )
212+ {
213+ command . Parameters . AddWithValue ( "@DatabaseName" , settings . Database ) ;
214+ }
215+
216+ await using ( SqlDataReader reader = await command . ExecuteReaderAsync ( ) )
115217 {
116- while ( reader . Read ( ) )
218+ while ( await reader . ReadAsync ( ) )
117219 {
118220 for ( int i = 0 ; i < reader . FieldCount ; i ++ )
119221 if ( reader . GetValue ( i ) != DBNull . Value )
120222 sb . Append ( $ "{ reader . GetName ( i ) } : { Convert . ToString ( reader . GetValue ( i ) ) } ") ;
121- //sb.AppendLine();
122223 }
123224 }
124225 }
125- AnsiConsole . MarkupLine ( $ " [green]SUCCESS[/] { sb . ToString ( ) . EscapeMarkup ( ) } ") ;
226+
227+ // Try to show connection encryption info, but ignore permission errors
228+ string info ;
229+ string currentDbName = connection . Database ;
230+ try
231+ {
232+ await using var infoCmd = new SqlCommand (
233+ "SELECT encrypt_option, net_transport FROM sys.dm_exec_connections WHERE session_id = @@SPID;" , connection ) ;
234+ await using var infoReader = await infoCmd . ExecuteReaderAsync ( ) ;
235+ if ( await infoReader . ReadAsync ( ) )
236+ {
237+ var encryptOption = Convert . ToString ( infoReader [ "encrypt_option" ] ) ;
238+ var transport = Convert . ToString ( infoReader [ "net_transport" ] ) ;
239+ info = $ " (db={ currentDbName } , encrypt_option={ encryptOption } , net_transport={ transport } )";
240+ }
241+ else
242+ {
243+ info = $ " (db={ currentDbName } )";
244+ }
245+ }
246+ catch ( SqlException )
247+ {
248+ info = $ " (db={ currentDbName } , connection details unavailable: requires VIEW SERVER STATE)";
249+ }
250+
251+ AnsiConsole . MarkupLine ( $ " [green]SUCCESS[/] { sb . ToString ( ) . EscapeMarkup ( ) } { info } ") ;
126252 }
127253 }
128254 catch ( SqlException ex )
129255 {
130256 AnsiConsole . MarkupLine ( $ " [red]ERROR: { ex . Message . EscapeMarkup ( ) } [/] ") ;
257+
258+ if ( ex . Message . IndexOf ( "certificate chain was issued by an authority that is not trusted" , StringComparison . OrdinalIgnoreCase ) >= 0 )
259+ {
260+ AnsiConsole . MarkupLine ( "[yellow]Troubleshooting tips:[/]" ) ;
261+ AnsiConsole . MarkupLine ( "- Ensure SQL Server uses a certificate trusted by this machine's Trusted Root store." ) ;
262+ AnsiConsole . MarkupLine ( "- Connect using a DNS name that matches the certificate's CN/SAN." ) ;
263+ AnsiConsole . MarkupLine ( "- Or set [teal]--hostname-in-certificate[/] to the certificate subject." ) ;
264+ AnsiConsole . MarkupLine ( "- For dev only, use [teal]--trust-server-certificate true[/] to bypass validation." ) ;
265+ AnsiConsole . MarkupLine ( "- If name keeps flipping to an IP, try [teal]--no-tnir[/]." ) ;
266+ }
131267 }
132268
133269 }
134270
271+ private static bool LooksLikeIp ( string server )
272+ {
273+ // Accept formats: "x.x.x.x" or "x.x.x.x,port"
274+ var parts = server . Split ( '\\ ' ) [ 0 ] ; // ignore instance suffix
275+ var ipAndPort = parts . Split ( ',' ) ;
276+ var ip = ipAndPort [ 0 ] . Trim ( ) ;
277+ return Regex . IsMatch ( ip , @"^\d{1,3}(\.\d{1,3}){3}$" ) ;
278+ }
279+
135280 public PingCommand ( ILogger < PingCommand > logger )
136281 {
137282 Logger = logger ;
0 commit comments