├── .shellcheckrc ├── CHANGELOG.md ├── LICENSE ├── examples ├── certs │ ├── expired-cert.pub │ ├── forever-cert.pub │ ├── in-the-future-cert.pub │ └── next-20-years-cert.pub └── keys │ ├── id_rsa_1.pub │ ├── id_rsa_2.pub │ ├── id_rsa_3.pub │ └── id_rsa_4.pub ├── ssh-certinfo ├── ssh-diff ├── ssh-facts ├── ssh-force-password ├── ssh-hostkeys ├── ssh-keyinfo ├── ssh-last ├── ssh-ping ├── ssh-version └── test.sh /.shellcheckrc: -------------------------------------------------------------------------------- 1 | enable=all 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.8 (unreleased) 2 | 3 | ## Added 4 | 5 | - **ssh-last**: like last but for SSH sessions 6 | 7 | ## Changed 8 | 9 | - ssh-facts: 10 | - Bugfix for newer FreeBSDs 11 | - ssh-ping: 12 | - Bugfix for Debian Bug #998219 making the package build reproducible 13 | - all 14 | - Shrink header comments 15 | 16 | ## Removed 17 | 18 | - all 19 | - Removed HashKnownHosts=no option 20 | 21 | > Some Distros set several options as standard in /etc/ssh/ssh_config. 22 | > Debian uses HashKnownHosts=yes by default 23 | > so entries in ~/.ssh/known_hosts get mixed with hashed and unhashed entries. 24 | > Removing this option, so ssh's default gets used 25 | 26 | # 1.7 (2021-10-31) 27 | 28 | ## Added 29 | 30 | - **ssh-force-password**: Enforces password authentication 31 | - ssh-ping 32 | - Option (-C) to connect/reconnect as soon as the host responds 33 | - Exit Codes 34 | - 1: More than 1 request lost 35 | - 2: All requests lost 36 | - Environment Variable 37 | - SSH_PING_NO_COLORS: if set, no colors are shown (like -n) 38 | 39 | # 1.6 (2020-01-23) 40 | 41 | ## Added 42 | 43 | - **ssh-certinfo**: Shows validity and information of SSH certificates 44 | - **ssh-keyinfo**: Prints keys in several formats 45 | - ssh-diff: Environment variable to disable remote file checking 46 | - ssh-facts: New explorers ( runlevel, disks ) 47 | - ssh-ping: Option to print human readable timestamp (-H) 48 | 49 | ## Changed 50 | 51 | - all 52 | - shellchecked and fixed errors and warnings (https://www.shellcheck.net) 53 | - ssh-diff: 54 | - Replaced tput with ANSI Escape codes for color output 55 | - Pipe output to cat to get a zero exit code for test.sh 56 | - ssh-facts: 57 | - Update explorers 58 | - ssh-ping: 59 | - Replaced tput with ANSI Escape codes for color output 60 | - Changed from Python to Perl for calculating time 61 | - ssh-version: 62 | - Updated usage (with examples) 63 | 64 | # 1.5 (2018-12-23) 65 | 66 | ## Added 67 | 68 | - **ssh-hostkeys**: Prints server host keys in several formats 69 | 70 | ## Removed 71 | 72 | - Moved packaging files for debian to https://salsa.debian.org/swick-guest/ssh-tools 73 | 74 | # 1.4 (2018-02-25) 75 | 76 | ## Added 77 | 78 | - ssh-facts: uptime and last_reboot fact 79 | 80 | ## Changed 81 | 82 | - minor fixes 83 | - improved documentation 84 | 85 | # 1.3 (2017-10-04) 86 | 87 | ## Added 88 | 89 | - better OpenBSD support 90 | 91 | ## Changed 92 | 93 | - consistent code formatting and better output 94 | - more robustness 95 | - portable to older Bash versions 96 | - changed license from AGPL-3 to GPL-3 and added debian/copyright 97 | 98 | # 1.2 (2017-09-03) 99 | 100 | ## Added 101 | 102 | - **ssh-diff**: Diff a file over SSH 103 | - **ssh-facts**: Get some facts about the remote system 104 | 105 | ## Changed 106 | 107 | - ssh-ping: works now under OSX 108 | - debianized package 109 | 110 | # 1.1 (2017-08-20) 111 | 112 | ## Added 113 | 114 | - ssh-ping: colors in statistics output 115 | 116 | # 1.0 (2017-08-14) 117 | 118 | Initial Release 119 | 120 | - **ssh-ping**: Check if host is reachable using ssh_config 121 | - **ssh-version**: Shows version of the SSH server you are connecting to 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /examples/certs/expired-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8LME+FswVlVMrB4MdgCqFx08JrbDFLdOce9j3e2/ybgAAAADAQABAAABAQDmzblLFSpP8sgvF1hNuFVPFC3DdL9Cswu8/eFgzY5nb825Bw8UxCq/DgQm7AuuInFvON96BR2DsAuRFQywJfh5IrwpIdVNT8/waG67v6SYPfvYba3NUlki/skW9Ox6Ch+z6GBWi1oCXb2I5u4w9WM9TFvP7iXBWhxAOt9EQE39zTuJQlNUOW3+93ZLdcWO961gJImr9oaks3YyeDtIBYPRZT89Y1f25gBtVsiLWk+wiO3ezypa5Rhlu4Z7kznDwr5GJ9SZ9rEib6huUGhCn7fz1nN7qL9WyvMZq7qbv1/ToXW9ROMPRmi4MuCgJYyQFJFvQuHcer+t8SBwzKLrHCYDAAAAAAAAAAAAAAABAAAAB2V4cGlyZWQAAAAAAAAAACSFjZEAAAAAKavpkQAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC48zvKsJplnaLTbXo0263ABcXqtl9D0O+hm2jxhqoJ/lS77IAuLoDZwYnaWizYvOknWgY66svE5EkY8kspro4tzSgLG0r9kncWlfNNEJb64kem/K9kiHJLpBvmaup0ZUFhg5BcpNmhDhlU5fm0+NI4+qgRvYXbM1rys8nxobtXGOJIFbNf9GkwA7eHpx/J6dPrDZ2QxUBzEy07fiGe7viGgF1xTaNjIwNuVWu3oz+XXzEqMC0mXsVxalA0qT0ol2arROYNWAp6/qNjGlu5VA1tFGUi1gGwIZ3++yEmJerTSV6OHns2g4JGcDvVU8i0KWvyi2o0xmgUbRgABjMFWSWDAAABDwAAAAdzc2gtcnNhAAABAHt8Sv2cLkGSNN68gAkdl9xW2lOhWvQ0CzFZoHIsC6tVU3Qv8Jyyj9Tx0UZb7h+ZX5Bminx81++Ig5kis64//u6xafzwu2QDPPnxmduJTQJEFg0wqfkTMaWagkWlsT5SrSE+3AbpZ5qn9Tit+RSKiwLxqkQwf52S1mtZXiY26Pz8iLyv177wxOjPjIBVDD8slCnQaGxkJY5kazmDBNaQDblWMThBgo75cgL5ckTpYMGiuxJsmpF5zCaSmLvSfOJKJG79Fl6amjNmdD5B827p6YbT4BaH1AecxI5/HjmpbaKzI36mb/bz4G4Boto/R8e9RWsakVItvmjz8wm2RgFZ37w= swick@8470p 2 | -------------------------------------------------------------------------------- /examples/certs/forever-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgEmDHw119SIC5DbYmdwvy2WpCJLwHEGxpqVlgZEV4A3gAAAADAQABAAABAQDmzblLFSpP8sgvF1hNuFVPFC3DdL9Cswu8/eFgzY5nb825Bw8UxCq/DgQm7AuuInFvON96BR2DsAuRFQywJfh5IrwpIdVNT8/waG67v6SYPfvYba3NUlki/skW9Ox6Ch+z6GBWi1oCXb2I5u4w9WM9TFvP7iXBWhxAOt9EQE39zTuJQlNUOW3+93ZLdcWO961gJImr9oaks3YyeDtIBYPRZT89Y1f25gBtVsiLWk+wiO3ezypa5Rhlu4Z7kznDwr5GJ9SZ9rEib6huUGhCn7fz1nN7qL9WyvMZq7qbv1/ToXW9ROMPRmi4MuCgJYyQFJFvQuHcer+t8SBwzKLrHCYDAAAAAAAAAAAAAAABAAAAB2ZvcmV2ZXIAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC48zvKsJplnaLTbXo0263ABcXqtl9D0O+hm2jxhqoJ/lS77IAuLoDZwYnaWizYvOknWgY66svE5EkY8kspro4tzSgLG0r9kncWlfNNEJb64kem/K9kiHJLpBvmaup0ZUFhg5BcpNmhDhlU5fm0+NI4+qgRvYXbM1rys8nxobtXGOJIFbNf9GkwA7eHpx/J6dPrDZ2QxUBzEy07fiGe7viGgF1xTaNjIwNuVWu3oz+XXzEqMC0mXsVxalA0qT0ol2arROYNWAp6/qNjGlu5VA1tFGUi1gGwIZ3++yEmJerTSV6OHns2g4JGcDvVU8i0KWvyi2o0xmgUbRgABjMFWSWDAAABDwAAAAdzc2gtcnNhAAABAI7HCo9S1CRvkol1DOiOebCII9+P5LW5j5FpnbAHQZlK5xfG/Dg6e6X4jxYwAyg1VBJOgMAi8kgzVyDrtY7JBDoTVPPMiLh10SLOwwnPklkjONVW1MntdEg8z81Kdbq1TlAq+SxymArVFmVs9NqWbcpY3q5sp4bhZ6XGq2Bx5XwCH1BG+B+GP2qU/nHcQWK32fK3DZYaTWYbsHogxPNrOdErEaavqBBJk/+XSKxMJjWT4PGjsqROFvEUbPFslMc7USGPS46cU+aNgOp/JPL6W9weH+7DmcUUpXIbog5qedxMTnszjQaBRSUHHq2BXrv+eOXmghtqFfv1QLpHRwWbry0= swick@8470p 2 | -------------------------------------------------------------------------------- /examples/certs/in-the-future-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgdMSETNhV3OaJETc9NvcmN3fUTtDf6SV541lPlv8l0Q0AAAADAQABAAABAQDmzblLFSpP8sgvF1hNuFVPFC3DdL9Cswu8/eFgzY5nb825Bw8UxCq/DgQm7AuuInFvON96BR2DsAuRFQywJfh5IrwpIdVNT8/waG67v6SYPfvYba3NUlki/skW9Ox6Ch+z6GBWi1oCXb2I5u4w9WM9TFvP7iXBWhxAOt9EQE39zTuJQlNUOW3+93ZLdcWO961gJImr9oaks3YyeDtIBYPRZT89Y1f25gBtVsiLWk+wiO3ezypa5Rhlu4Z7kznDwr5GJ9SZ9rEib6huUGhCn7fz1nN7qL9WyvMZq7qbv1/ToXW9ROMPRmi4MuCgJYyQFJFvQuHcer+t8SBwzKLrHCYDAAAAAAAAAAAAAAABAAAADWluLXRoZS1mdXR1cmUAAAAAAAAAAJCrGjsAAAAAldF2OwAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC48zvKsJplnaLTbXo0263ABcXqtl9D0O+hm2jxhqoJ/lS77IAuLoDZwYnaWizYvOknWgY66svE5EkY8kspro4tzSgLG0r9kncWlfNNEJb64kem/K9kiHJLpBvmaup0ZUFhg5BcpNmhDhlU5fm0+NI4+qgRvYXbM1rys8nxobtXGOJIFbNf9GkwA7eHpx/J6dPrDZ2QxUBzEy07fiGe7viGgF1xTaNjIwNuVWu3oz+XXzEqMC0mXsVxalA0qT0ol2arROYNWAp6/qNjGlu5VA1tFGUi1gGwIZ3++yEmJerTSV6OHns2g4JGcDvVU8i0KWvyi2o0xmgUbRgABjMFWSWDAAABDwAAAAdzc2gtcnNhAAABAJDFYzdjhLoNzhktDuJhlaq574EsiEubJ3z8ElU+VMrsF003LoI6LlBryhwj8uglIZzrQiDrwwEHnqe6NiFr91ysGbJgSF4hnzZq5ye/sAYMw/JpSmTJg3cycnMcPNcXNeFoiDlG9muNfgpeoIEqAjzVc0fbHCRGuqvLTT8y9hxGGNFU3ISmJLbcIgdHB1mgAEs7Sih2zy4Cp1kyCZ3I/Bc+qO50IvFXhVZRlJY1UhfmOIGr6F1zuSsCYimmEs7STyA0fqCXITU4Lj+AN1/kac3B+ASmZyV5puEtfzqogc+g/iMaYNcNnr7FX7UXFug2AV9BhWbrLa+T0R0swUEGtwI= swick@8470p 2 | -------------------------------------------------------------------------------- /examples/certs/next-20-years-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgR3GZzAC+KgP+1zk2PMIojpD9Qw83jH6aRcbgORbd/e0AAAADAQABAAABAQDmzblLFSpP8sgvF1hNuFVPFC3DdL9Cswu8/eFgzY5nb825Bw8UxCq/DgQm7AuuInFvON96BR2DsAuRFQywJfh5IrwpIdVNT8/waG67v6SYPfvYba3NUlki/skW9Ox6Ch+z6GBWi1oCXb2I5u4w9WM9TFvP7iXBWhxAOt9EQE39zTuJQlNUOW3+93ZLdcWO961gJImr9oaks3YyeDtIBYPRZT89Y1f25gBtVsiLWk+wiO3ezypa5Rhlu4Z7kznDwr5GJ9SZ9rEib6huUGhCn7fz1nN7qL9WyvMZq7qbv1/ToXW9ROMPRmi4MuCgJYyQFJFvQuHcer+t8SBwzKLrHCYDAAAAAAAAAAAAAAABAAAADW5leHQtMjAteWVhcnMAAAAAAAAAAF0rggQAAAAAgVzwaQAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC48zvKsJplnaLTbXo0263ABcXqtl9D0O+hm2jxhqoJ/lS77IAuLoDZwYnaWizYvOknWgY66svE5EkY8kspro4tzSgLG0r9kncWlfNNEJb64kem/K9kiHJLpBvmaup0ZUFhg5BcpNmhDhlU5fm0+NI4+qgRvYXbM1rys8nxobtXGOJIFbNf9GkwA7eHpx/J6dPrDZ2QxUBzEy07fiGe7viGgF1xTaNjIwNuVWu3oz+XXzEqMC0mXsVxalA0qT0ol2arROYNWAp6/qNjGlu5VA1tFGUi1gGwIZ3++yEmJerTSV6OHns2g4JGcDvVU8i0KWvyi2o0xmgUbRgABjMFWSWDAAABDwAAAAdzc2gtcnNhAAABALCrG6ZqRt1Cm9mhYw821/XkMR+KyERtU3u/RufJqJLecPX7NeaZAFAG3EVXKOmomMgnLiN8bjElZ2j/Qss/OgTlWlSCtv5mLxfNk56r0DRnZgPyd9sREoww57KpFAmprPELKTt4pqrPULF/YmDP6CVNbeOclsryJoP54SuVTq5EjrAlB4hzU0/4DgVsyyxxIrGXP+Q51Q1S7RtDWV7aD8ljiW3MLLFLploPZ9tWtGZeoLiSBCvoFId2VZUSguzaGdPrj1g5yK0RUaM6nSYhEVS/YqzEUxEdlpcnooBLfC9OkycKEgZDwfdmiLH7NKm3VBciL5fvpmwoIdRpCsKM38Q= swick@8470p 2 | -------------------------------------------------------------------------------- /examples/keys/id_rsa_1.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2/hTtNC2+Oh7jRiq3l5jAFfUxcx8xZhNcuJV8ci67quIoLlsdPsuG7/6+1vg3GESnKozDfOntRdQMxRRdkiCNWGtDvY3BOMJ16T7dTrSNZnPkStpRkZguEqH3Zdey/ShSpgo5cWk0M/puq0KT8YBgH1669cQZyHIToXU0MKDwTEr0G3hwGuxOWw1HmXnh6yCeMtSfSspLP8YSZgw+S10hRJZxpZYPRhmZ3axJuprh8SdfDRpGUV21V6XJBc7ObrSml56DJJjvkpEOkIxPVllJDWhm2IxELd3Zh1yFUe4X2Halhpwx5KurUTDoUgoDCC1OFYX/w3oVqgywxGkzrg0zw== www@iZ23omxvgedZ 2 | -------------------------------------------------------------------------------- /examples/keys/id_rsa_2.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAgEAwrr66r8n6B8Y0zMF3dOpXEapIQD9DiYQ6D6/zwor9o39jSkHNiMMER/GETBbzP83LOcekm02aRjo55ArO7gPPVvCXbrirJu9pkm4AC4BBre5xSLS7soyzwbigFruM8G63jSXqpHqJ/ooi168sKMC2b0Ncsi+JlTfNYlDXJVLKEeZgZOInQyMmtisaDTUQWTIv1snAizf4iIYENuAkGYGNCL77u5Y5VOu5eQipvFajTnps9QvUx/zdSFYn9e2sulWM3Bxc/S4IJ67JWHVRpfJxGi3hinRBH8WQdXuUwdJJTiJHKPyYrrM7Q6Xq4TOMFtcRuLDC6u3BXM1L0gBvHPNOnD5l2Lp5EjUkQ9CBf2j4A4gfH+iWQZyk08esAG/iwArAVxkl368+dkbMWOXL8BN4x5zYgdzoeypQZZ2RKH780MCTSo4WQ19DP8pw+9q3bSFC9H3xYAxrKAJNWjeTUJOTrTe+mWXXU770gYyQTxa2ycnYrlZucn1S3vsvn6eq7NZZ8NRbyv1n15Ocg+nHK4fuKOrwPhU3NbKQwtjb0Wsxx1gAmQqIOLTpAdsrAauPxC7TPYA5qQVCphvimKuhQM/1gMV225JrnjspVlthCzuFYUjXOKC3wxz6FFEtwnXu3uC5bVVkmkNadJmD21gD23yk4BraGXVYpRMIB+X+OTUUI8= dhopson@VMUbuntu-DSH 2 | -------------------------------------------------------------------------------- /examples/keys/id_rsa_3.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtRr15KNnY3mR3r+H+Cy0C0Wyow7gScBTXx+euP2RoO9xHurphg7rvvGENWTfOlk/qzj9V2+BbwkU7tZa7uRC0fLxodKKr+QTO2BXxRGdipkQpjdflUxeascMTEG6WOIsNfmn2+uaPapKNedpTE2bf22hHGlooDqqmFjdfFU17dBWSMKJ8yQCgOCFJ5DVM3c0/t+teShLkXmVzU0G/rKrZDXjKZUlS1B7t46NhgY99ATi/go1/hs3lNMQP+gpc/FM9IM6Y6eWXoS3F6nTbibavVdsx/qig8sbv6FqoEi2cDx2QyPlXjLCVlt2Z1kv+KhXX8fltjmBifFgiq/H35cZT hs@schlittermann.de openpgp:0x0A51ECF9 2 | -------------------------------------------------------------------------------- /examples/keys/id_rsa_4.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDR8v5+P4jvK0ITa62f3HvX7hfHHscjkO/IHKklXGDGpbjqVj95E6Qg2afG7pLNP+U2Ati+E/9lzA6xuRz7/BFOn0zJLP4bzSnCIumMeTe+4l6VxkPxAd8x6MJ8rqN+PnyY7J1ekbruINv0vd4wnvH2q2h8qOs1y65WdzN4WfDJP9v4u4ySLji14/kcmIafJGIMa8bemvdmeVA6R5WQ6WY8A6TCVi8lt9SelPyRfNwbDmhOp6obbxDBct+oaCMFepSfwKxYJ8LO/e9srNx2vOcji1xDUT0YXEnY+DfBNiBDB/2uTOBuw7zl/w0k4V+R2s/D4nogwGgm0QevadAddx+QXXwzEyWWX1cuk3Ufx3zQkEDzV+x9E+ARJLU9Xs0cvhR0RmovBkqsaJIGwppPPkBO+9wrwjFN2NDx8rLUlIWQyPIjwhIbWpOzjxaIJLJkqQhF7onr90huEUFiW+Zjq+OE9ovls1ORYHPgH1Vd7S+J4jlNZEEDX6KJ7gOCk5yQK80H1cdWFuqPD3k+qKfOaRh89Xk2dv7y6TY+RB4JATUWqadT6kmmpKlqnaKe67yuE1KssDczl+8k+1eiNhSexdfmy9ny75Oav4ZyXBZLkya4a6Q2h+s7TAyPeh/n5Ixe5N8CUejBLx/rbeHd+Z3X67SNeLyTW+5nv48EqCT9qbGoGw== heiko@jumper 2 | -------------------------------------------------------------------------------- /ssh-certinfo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +------------------------------------------------------------------+ 4 | # | Title : ssh-certinfo | 5 | # | Description : shows validity and information of SSH certificates | 6 | # | Author : Sven Wick | 7 | # | URL : https://github.com/vaporup/ssh-tools | 8 | # +------------------------------------------------------------------+ 9 | 10 | # shellcheck disable=SC2207 11 | 12 | # 13 | # Some colors for better output 14 | # 15 | 16 | RED='\033[0;91m' 17 | GREEN='\033[0;92m' 18 | YELLOW='\033[0;93m' 19 | RESET='\033[0m' 20 | 21 | # 22 | # Defaults 23 | # 24 | 25 | WARN=30 # days before cert expires 26 | 27 | # 28 | # Usage/Help message 29 | # 30 | 31 | function usage() { 32 | 33 | cat << EOF 34 | 35 | Usage: ${0##*/} [OPTIONS] CERT-FILE [...] 36 | 37 | OPTIONS: 38 | 39 | -c show colors 40 | -h Show this message 41 | -w days warning threshold (default: 30) 42 | -v Verbose output 43 | 44 | Examples: 45 | 46 | Default: 47 | 48 | ${0##*/} ~/.ssh/id_rsa-cert.pub 49 | 50 | ${0##*/} ~/.ssh/*.pub 51 | 52 | Certificates which expire within the next 2 months (colored output): 53 | 54 | ${0##*/} -c -w 60 ~/.ssh/id_rsa-cert.pub 55 | 56 | ${0##*/} -c -w 60 ~/.ssh/*.pub 57 | 58 | EOF 59 | 60 | } 61 | 62 | if [[ -z $1 || $1 == "--help" ]]; then 63 | usage 64 | exit 1 65 | fi 66 | 67 | # 68 | # Command line Options 69 | # 70 | 71 | # shellcheck disable=SC2249 72 | while getopts ":chvw:" opt; do 73 | case ${opt} in 74 | c ) 75 | colors="yes" 76 | ;; 77 | h ) 78 | usage 79 | exit 1 80 | ;; 81 | v ) 82 | verbose="yes" 83 | ;; 84 | w ) 85 | [[ ${OPTARG} =~ ^[0-9]+$ ]] && WARN=${OPTARG} 86 | ;; 87 | \? ) 88 | echo "Invalid option: ${OPTARG}" 1>&2 89 | usage 90 | exit 1 91 | ;; 92 | esac 93 | done 94 | 95 | function return_epoch_from_date_string() { 96 | 97 | # 98 | # date behaves differently on *BSD, Busybox, etc.. 99 | # Trying multiple variants and use first that succeeds 100 | # 101 | 102 | DATES=() 103 | 104 | # 105 | # GNU date 106 | # 107 | 108 | DATES+=( $( date -d "$1" +%s 2>/dev/null || echo NO_DATE ) ) 109 | 110 | # 111 | # BSD date 112 | # 113 | 114 | DATES+=( $( date -j -f "%Y-%m-%dT%T" "$1" "+%s" 2>/dev/null || echo NO_DATE ) ) 115 | 116 | # 117 | # BusyBox 118 | # 119 | 120 | DATES+=( $( date -d "${1//T/ }" +%s 2>/dev/null || echo NO_DATE ) ) 121 | 122 | for DATE in "${DATES[@]}"; do 123 | if [[ ${DATE} -gt 0 ]]; then 124 | echo "${DATE}" 125 | break 126 | fi 127 | done 128 | 129 | } 130 | 131 | function print_cert() { 132 | 133 | ssh-keygen -L -f "${cert}" 134 | echo 135 | } 136 | 137 | function get_cert_data() { 138 | 139 | valid_from=$( echo "${valid}" | awk '{print $3}' ) 140 | valid_to=$( echo "${valid}" | awk '{print $5}' ) 141 | valid_from_epoch=$( return_epoch_from_date_string "${valid_from}" ) 142 | valid_to_epoch=$( return_epoch_from_date_string "${valid_to}" ) 143 | 144 | valid_to_epoch_warning=$(( valid_to_epoch - WARN_SECONDS )) 145 | expires_in_days=$(( WARN - ( ( now - valid_to_epoch_warning ) / 60 / 60 / 24 ) -1 )) 146 | 147 | } 148 | 149 | function print_if_certs_were_found() { 150 | 151 | if grep -q "yes" "${certs_found}"; then 152 | echo 153 | else 154 | if [[ ${colors} == yes ]]; then 155 | echo -e -n "${YELLOW}" 156 | echo -e "No SSH certificates found.\n" 157 | echo -e -n "${RESET}" 158 | else 159 | echo -e "No SSH certificates found.\n" 160 | fi 161 | fi 162 | } 163 | 164 | function cert_is_valid() { 165 | 166 | if [[ ${now} -gt ${valid_from_epoch} && ${now} -lt ${valid_to_epoch} ]]; then 167 | return 0 168 | else 169 | return 1 170 | fi 171 | 172 | } 173 | 174 | function cert_is_invalid() { 175 | 176 | if [[ ${now} -lt ${valid_from_epoch} ]]; then 177 | return 0 178 | else 179 | return 1 180 | fi 181 | 182 | } 183 | 184 | function cert_is_expired() { 185 | 186 | if [[ ${now} -gt ${valid_to_epoch} ]]; then 187 | return 0 188 | else 189 | return 1 190 | fi 191 | 192 | } 193 | 194 | function cert_expires() { 195 | 196 | if [[ ${now} -gt ${valid_to_epoch_warning} ]]; then 197 | return 0 198 | else 199 | return 1 200 | fi 201 | 202 | } 203 | 204 | WARN_SECONDS=$(( WARN * 24 * 60 * 60)) 205 | CERTS=( "${@:${OPTIND}}" ) 206 | now=$(date +%s) 207 | certs_found="$(mktemp)" 208 | trap 'rm -f ${certs_found}' EXIT 209 | 210 | echo 211 | 212 | if [[ ${verbose} == yes ]]; then 213 | 214 | for cert in "${CERTS[@]}"; do 215 | 216 | valid=$( print_cert 2>/dev/null | grep -i valid) 217 | 218 | if [[ -z ${valid} ]]; then 219 | continue 220 | else 221 | echo "yes" > "${certs_found}" 222 | fi 223 | 224 | if [[ ${valid} == *"forever"* ]]; then 225 | if [[ ${colors} == yes ]]; then 226 | echo -e -n "${GREEN}" 227 | print_cert 228 | echo -e -n "${RESET}" 229 | continue 230 | else 231 | print_cert 232 | continue 233 | fi 234 | fi 235 | 236 | get_cert_data 237 | 238 | if cert_is_invalid; then 239 | 240 | if [[ ${colors} == yes ]]; then 241 | echo -e -n "${YELLOW}" 242 | print_cert 243 | echo -e -n "${RESET}" 244 | continue 245 | else 246 | print_cert 247 | continue 248 | fi 249 | fi 250 | 251 | if cert_is_expired; then 252 | 253 | if [[ ${colors} == yes ]]; then 254 | echo -e -n "${RED}" 255 | print_cert 256 | echo -e -n "${RESET}" 257 | continue 258 | else 259 | print_cert 260 | continue 261 | fi 262 | 263 | fi 264 | 265 | if cert_expires; then 266 | 267 | if [[ ${colors} == yes ]]; then 268 | echo -e -n "${YELLOW}" 269 | print_cert 270 | echo -e -n "${RESET}" 271 | continue 272 | else 273 | print_cert 274 | continue 275 | fi 276 | 277 | fi 278 | 279 | if cert_is_valid; then 280 | 281 | if [[ ${colors} == yes ]]; then 282 | echo -e -n "${GREEN}" 283 | print_cert 284 | echo -e -n "${RESET}" 285 | continue 286 | else 287 | print_cert 288 | continue 289 | fi 290 | 291 | fi 292 | 293 | done 294 | 295 | print_if_certs_were_found 296 | 297 | else 298 | 299 | for cert in "${CERTS[@]}"; do 300 | 301 | valid=$( print_cert 2>/dev/null | grep -i valid) 302 | 303 | if [[ -z ${valid} ]]; then 304 | continue 305 | else 306 | echo "yes" > "${certs_found}" 307 | fi 308 | 309 | if [[ ${valid} == *"forever"* ]]; then 310 | if [[ ${colors} == yes ]]; then 311 | echo -e "${GREEN}${cert} SSH_CERT_VALID forever -> forever${RESET}" 312 | continue 313 | else 314 | echo "${cert} SSH_CERT_VALID forever -> forever" 315 | continue 316 | fi 317 | fi 318 | 319 | get_cert_data 320 | 321 | if cert_is_invalid; then 322 | 323 | if [[ ${colors} == yes ]]; then 324 | echo -e "${YELLOW}${cert} SSH_CERT_INVALID ${valid_from} -> ${valid_to}${RESET}" 325 | continue 326 | else 327 | echo "${cert} SSH_CERT_INVALID ${valid_from} -> ${valid_to}" 328 | continue 329 | fi 330 | fi 331 | 332 | if cert_is_expired; then 333 | 334 | if [[ ${colors} == yes ]]; then 335 | echo -e "${RED}${cert} SSH_CERT_EXPIRED ${valid_from} -> ${valid_to}${RESET}" 336 | continue 337 | else 338 | echo "${cert} SSH_CERT_EXPIRED ${valid_from} -> ${valid_to}" 339 | continue 340 | fi 341 | 342 | fi 343 | 344 | if cert_expires; then 345 | 346 | if [[ ${colors} == yes ]]; then 347 | echo -e "${YELLOW}${cert} SSH_CERT_EXPIRES_IN_${expires_in_days}_DAYS ${valid_from} -> ${valid_to} ${RESET}" 348 | continue 349 | else 350 | echo "${cert} SSH_CERT_EXPIRES_IN_${expires_in_days}_DAYS ${valid_from} -> ${valid_to}" 351 | continue 352 | fi 353 | 354 | fi 355 | 356 | if cert_is_valid; then 357 | 358 | if [[ ${colors} == yes ]]; then 359 | echo -e "${GREEN}${cert} SSH_CERT_VALID ${valid_from} -> ${valid_to}${RESET}" 360 | continue 361 | else 362 | echo "${cert} SSH_CERT_VALID ${valid_from} -> ${valid_to}" 363 | continue 364 | fi 365 | 366 | fi 367 | 368 | done | column -t 369 | 370 | print_if_certs_were_found 371 | 372 | fi 373 | -------------------------------------------------------------------------------- /ssh-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +---------------------------------------------------------------------------------------------------+ 4 | # | Title : ssh-diff | 5 | # | Description : Diff a file over SSH | 6 | # | Author : Sven Wick | 7 | # | Contributors : Denis Meiswinkel | 8 | # | URL : https://github.com/vaporup/ssh-tools | 9 | # | Based On : https://gist.github.com/jhqv/dbd59f5838ae8c83f736bfe951bd80ff | 10 | # | https://serverfault.com/questions/59140/how-do-diff-over-ssh#comment1027981_216496 | 11 | # +---------------------------------------------------------------------------------------------------+ 12 | 13 | # shellcheck disable=SC2029 14 | 15 | # 16 | # Some colors for better output 17 | # 18 | 19 | RED='\033[0;91m' 20 | BOLD='\033[1m' 21 | RESET='\033[0m' 22 | 23 | # 24 | # Usage/Help message 25 | # 26 | 27 | function usage() { 28 | 29 | cat << EOF 30 | 31 | Usage: ${0##*/} [OPTIONS] FILE [user@]hostname[:FILE] 32 | 33 | There is an extra roundtrip to the remote system 34 | to check for the existence of the file to be diffed. 35 | So if you are not using SSH Keys 36 | you may get prompted twice for a password. 37 | 38 | Use "CHECK_REMOTE_FILE_EXISTS=NO ${0##*/}" to disable that behavior 39 | 40 | Diff Options: 41 | 42 | All options your local diff command supports ( except '-r' ). 43 | See 'man diff' and 'diff --help' for more information. 44 | 45 | SSH Options: 46 | 47 | -4 Use IPv4 only 48 | -6 Use IPv6 only 49 | -p port Port to connect to on the remote host. 50 | This can be specified on a per-host basis in the configuration file. 51 | 52 | Examples: 53 | 54 | Default: 55 | 56 | ${0##*/} /etc/hosts 192.168.1.10 57 | 58 | ${0##*/} /etc/hosts root@192.168.1.10 59 | 60 | ${0##*/} /etc/hosts root@192.168.1.10:/etc/hosts.old 61 | 62 | Side-by-Side: 63 | 64 | ${0##*/} -y /etc/hosts 192.168.1.10 65 | 66 | ${0##*/} -y /etc/hosts root@192.168.1.10 67 | 68 | ${0##*/} -y /etc/hosts root@192.168.1.10:/etc/hosts.old 69 | 70 | Unified: 71 | 72 | ${0##*/} -u /etc/hosts 192.168.1.10 73 | 74 | ${0##*/} -u /etc/hosts root@192.168.1.10 75 | 76 | ${0##*/} -u /etc/hosts root@192.168.1.10:/etc/hosts.old 77 | 78 | EOF 79 | 80 | } 81 | 82 | if [[ -z $1 || $1 == "--help" ]]; then 83 | usage 84 | exit 1 85 | fi 86 | 87 | if ! [[ $# -ge 2 ]]; then 88 | echo -e "\n ${RED}Error: Not all filenames given${RESET}" >&2 89 | usage 90 | exit 1 91 | fi 92 | 93 | function supports_colordiff() { 94 | 95 | type colordiff &> /dev/null 96 | 97 | } 98 | 99 | function show_header() { 100 | 101 | echo "" 102 | echo -e "Comparing ${BOLD}${remote_host}:${remote_file}${RESET} (<) with ${BOLD}${local_file}${RESET} (>)" 103 | echo "" 104 | 105 | } 106 | 107 | function login_successful_and_remote_file_exists() { 108 | 109 | # shellcheck disable=SC2154 110 | if [[ ${CHECK_REMOTE_FILE_EXISTS} == "NO" ]]; then 111 | return 0 112 | fi 113 | 114 | if [[ -z "${username}" ]]; then 115 | ssh "${ssh_params[@]}" "${remote_host}" "test -e ${remote_file}" &> /dev/null 116 | else 117 | ssh "${ssh_params[@]}" "${username}@${remote_host}" "test -e ${remote_file}" &> /dev/null 118 | fi 119 | 120 | RETVAL=$? 121 | 122 | [[ ${RETVAL} -eq 255 ]] && { echo -e "\n ${RED}Error: Could not connect to remote server${RESET}\n" >&2 ; exit "${RETVAL}"; } 123 | [[ ${RETVAL} -ge 1 ]] && { echo -e "\n ${RED}Error: Remote file ${remote_file} not found${RESET}\n" >&2 ; exit "${RETVAL}"; } 124 | [[ ${RETVAL} -eq 0 ]] && true 125 | 126 | } 127 | 128 | function diff_files() { 129 | 130 | if [[ -z "${username}" ]]; then 131 | if login_successful_and_remote_file_exists; then 132 | show_header 133 | if supports_colordiff; then 134 | ssh "${ssh_params[@]}" "${remote_host}" "cat ${remote_file}" | diff "${diff_params[@]}" --label "${remote_host}:${remote_file}" - "${local_file}" | colordiff 135 | else 136 | # Pipe the output to cat after diffing 137 | # 138 | # test.sh fails when colordiff is not installed. 139 | # Reason is that a normal diff returns with 1 140 | # when files differ but colordiff changes the return code to 0. 141 | 142 | ssh "${ssh_params[@]}" "${remote_host}" "cat ${remote_file}" | diff "${diff_params[@]}" --label "${remote_host}:${remote_file}" - "${local_file}" | cat 143 | fi 144 | fi 145 | else 146 | if login_successful_and_remote_file_exists; then 147 | show_header 148 | if supports_colordiff; then 149 | ssh "${ssh_params[@]}" "${username}@${remote_host}" "cat ${remote_file}" | diff "${diff_params[@]}" --label "${remote_host}:${remote_file}" - "${local_file}" | colordiff 150 | else 151 | # Pipe the output to cat after diffing 152 | # 153 | # test.sh fails when colordiff is not installed. 154 | # Reason is that a normal diff returns with 1 155 | # when files differ but colordiff changes the return code to 0. 156 | 157 | ssh "${ssh_params[@]}" "${username}@${remote_host}" "cat ${remote_file}" | diff "${diff_params[@]}" --label "${remote_host}:${remote_file}" - "${local_file}" | cat 158 | fi 159 | fi 160 | fi 161 | 162 | } 163 | 164 | # 165 | # MAIN 166 | # 167 | 168 | # 169 | # Get all params from command line 170 | # 171 | 172 | params=( "$@" ) 173 | 174 | # 175 | # Get last 2 params, store them away and remove them so only diff params are left 176 | # 177 | 178 | remote_params="${params[ ${#params[@]}-1 ]}" && unset 'params[ ${#params[@]}-1 ]' 179 | local_filename="${params[ ${#params[@]}-1 ]}" && unset 'params[ ${#params[@]}-1 ]' 180 | 181 | diff_params=( "${params[@]}" ) 182 | 183 | # 184 | # Fish within diff params for ssh options and extract them 185 | # 186 | 187 | diff_params_index=0 188 | 189 | ssh_params=() 190 | ssh_params_indices=() 191 | 192 | for param in "${diff_params[@]}"; do 193 | 194 | next_diff_params_index=$(( diff_params_index +1 )) 195 | 196 | # 197 | # find -p 198 | # 199 | 200 | if [[ ${param} == "-p" ]] && [[ -z "${diff_params[${next_diff_params_index}]//[0-9]}" ]]; then 201 | ssh_params_indices+=( "${diff_params_index}" "${next_diff_params_index}" ) 202 | ssh_params+=( "${param}" "${diff_params[${next_diff_params_index}]}" ) 203 | fi 204 | 205 | # 206 | # find -4 or -6 207 | # 208 | 209 | if [[ ${param} == "-4" ]] || [[ ${param} == "-6" ]]; then 210 | ssh_params_indices+=( "${diff_params_index}" ) 211 | ssh_params+=( "${param}" ) 212 | fi 213 | 214 | diff_params_index=$(( diff_params_index +1 )) 215 | 216 | done 217 | 218 | # shellcheck disable=SC2034 219 | for ssh_param in "${ssh_params_indices[@]}"; do 220 | 221 | # shellcheck disable=SC2184 222 | # shellcheck disable=SC2102 223 | unset diff_params[$ssh_param] 224 | 225 | done 226 | 227 | # 228 | # Getting username, hostname and filename from command line without using grep and awk 229 | # 230 | # user@host:filename -> user gets stored in $username 231 | # -> host gets stored in $remote_host 232 | # -> filename gets stored in $remote_file 233 | # 234 | 235 | if [[ ${remote_params} == *"@"* ]]; then 236 | remote_part="${remote_params##*@}" 237 | username="${remote_params%%@*}" 238 | else 239 | remote_part=${remote_params} 240 | fi 241 | 242 | if [[ ${remote_part} == *":"* ]]; then 243 | remote_file="${remote_part##*:}" 244 | remote_host="${remote_part%%:*}" 245 | else 246 | remote_host=${remote_part} 247 | fi 248 | 249 | local_file=$(readlink -f "${local_filename}") # get absolute path to file in case it was relative 250 | 251 | if [[ -z "${local_file}" ]]; then 252 | echo -e "\n ${RED}Error: Given file not found${RESET}\n" >&2 253 | exit 1 254 | fi 255 | 256 | if [[ ! -f ${local_file} ]]; then 257 | echo -e "\n ${RED}Error: Local file ${local_file} not found${RESET}\n" >&2 258 | exit 1 259 | fi 260 | 261 | if [[ -z "${remote_file}" ]]; then 262 | remote_file=${local_file} 263 | fi 264 | 265 | # 266 | # Finally diff them! 267 | # 268 | 269 | diff_files 270 | -------------------------------------------------------------------------------- /ssh-facts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +-----------------------------------------------------------------------------------------------+ 4 | # | Title : ssh-facts | 5 | # | Description : Get some facts about the remote system | 6 | # | Author : Sven Wick | 7 | # | Contributors : Denis Meiswinkel | 8 | # | URL : https://github.com/vaporup/ssh-tools | 9 | # | Based On : https://code.ungleich.ch/ungleich-public/cdist/tree/master/cdist/conf/explorer | 10 | # | https://serverfault.com/a/343678 | 11 | # | https://stackoverflow.com/a/8057052 | 12 | # +-----------------------------------------------------------------------------------------------+ 13 | 14 | # 15 | # Usage/Help message 16 | # 17 | 18 | function usage() { 19 | 20 | cat << EOF 21 | 22 | Usage: ${0##*/} [user@]hostname 23 | 24 | For further processing of the data you can use standard shell tools like awk, grep, sed 25 | or convert it to JSON with 'jo' (command-line processor to output JSON from a shell) 26 | and feed it to 'jq' (lightweight and flexible command-line JSON processor) 27 | 28 | Examples: 29 | 30 | ${0##*/} 127.0.0.1 31 | 32 | ${0##*/} 127.0.0.1 | grep ^OS_VERSION | awk -F'=' '{ print \$2 }' 33 | 34 | ${0##*/} 127.0.0.1 | jo -p 35 | 36 | ${0##*/} 127.0.0.1 | jo | jq 37 | 38 | ${0##*/} 127.0.0.1 | jo | jq .OS_VERSION 39 | 40 | EOF 41 | 42 | } 43 | 44 | if [[ -z $1 || $1 == "--help" ]]; then 45 | usage 46 | exit 1 47 | fi 48 | 49 | ssh "$@" 'bash -s' 2>/dev/null <<'END' | sed 's/[[:space:]]*=[[:space:]]*/=/' 50 | 51 | function _os() { 52 | 53 | if grep -q ^Amazon /etc/system-release 2>/dev/null; then 54 | echo amazon 55 | exit 0 56 | fi 57 | 58 | if [ -f /etc/arch-release ]; then 59 | echo archlinux 60 | exit 0 61 | fi 62 | 63 | if [ -f /etc/cdist-preos ]; then 64 | echo cdist-preos 65 | exit 0 66 | fi 67 | 68 | if [ -d /gnu/store ]; then 69 | echo guixsd 70 | exit 0 71 | fi 72 | 73 | ### Debian and derivatives 74 | if grep -q ^DISTRIB_ID=Ubuntu /etc/lsb-release 2>/dev/null; then 75 | echo ubuntu 76 | exit 0 77 | fi 78 | 79 | # devuan ascii has both devuan_version and debian_version, so we need to check devuan_version first! 80 | if [ -f /etc/devuan_version ]; then 81 | echo devuan 82 | exit 0 83 | fi 84 | 85 | if [ -f /etc/debian_version ]; then 86 | echo debian 87 | exit 0 88 | fi 89 | 90 | ### 91 | 92 | if [ -f /etc/gentoo-release ]; then 93 | echo gentoo 94 | exit 0 95 | fi 96 | 97 | if [ -f /etc/openwrt_version ]; then 98 | echo openwrt 99 | exit 0 100 | fi 101 | 102 | if [ -f /etc/owl-release ]; then 103 | echo owl 104 | exit 0 105 | fi 106 | 107 | ### Redhat and derivatives 108 | if grep -q ^Scientific /etc/redhat-release 2>/dev/null; then 109 | echo scientific 110 | exit 0 111 | fi 112 | 113 | if grep -q ^CentOS /etc/redhat-release 2>/dev/null; then 114 | echo centos 115 | exit 0 116 | fi 117 | 118 | if grep -q ^Fedora /etc/redhat-release 2>/dev/null; then 119 | echo fedora 120 | exit 0 121 | fi 122 | 123 | if grep -q ^Mitel /etc/redhat-release 2>/dev/null; then 124 | echo mitel 125 | exit 0 126 | fi 127 | 128 | if [ -f /etc/redhat-release ]; then 129 | echo redhat 130 | exit 0 131 | fi 132 | ### 133 | 134 | if [ -f /etc/SuSE-release ]; then 135 | echo suse 136 | exit 0 137 | fi 138 | 139 | if [ -f /etc/slackware-version ]; then 140 | echo slackware 141 | exit 0 142 | fi 143 | 144 | uname_s="$(uname -s)" 145 | 146 | # Assume there is no tr on the client -> do lower case ourselves 147 | case "$uname_s" in 148 | Darwin) 149 | echo macosx 150 | exit 0 151 | ;; 152 | NetBSD) 153 | echo netbsd 154 | exit 0 155 | ;; 156 | FreeBSD) 157 | echo freebsd 158 | exit 0 159 | ;; 160 | OpenBSD) 161 | echo openbsd 162 | exit 0 163 | ;; 164 | SunOS) 165 | echo solaris 166 | exit 0 167 | ;; 168 | esac 169 | 170 | if [ -f /etc/os-release ]; then 171 | # already lowercase, according to: 172 | # https://www.freedesktop.org/software/systemd/man/os-release.html 173 | awk -F= '/^ID=/ {print $2;}' /etc/os-release 174 | exit 0 175 | fi 176 | 177 | echo "Unknown OS" >&2 178 | exit 1 179 | 180 | } 181 | 182 | function _os_version() { 183 | 184 | case "$(_os)" in 185 | amazon) 186 | cat /etc/system-release 187 | ;; 188 | archlinux) 189 | # empty, but well... 190 | cat /etc/arch-release 191 | ;; 192 | debian) 193 | cat /etc/debian_version 194 | ;; 195 | devuan) 196 | cat /etc/devuan_version 197 | ;; 198 | fedora) 199 | cat /etc/fedora-release 200 | ;; 201 | gentoo) 202 | cat /etc/gentoo-release 203 | ;; 204 | macosx) 205 | sw_vers -productVersion 206 | ;; 207 | *bsd|solaris) 208 | uname -r 209 | ;; 210 | openwrt) 211 | cat /etc/openwrt_version 212 | ;; 213 | owl) 214 | cat /etc/owl-release 215 | ;; 216 | redhat|centos|mitel|scientific) 217 | cat /etc/redhat-release 218 | ;; 219 | slackware) 220 | cat /etc/slackware-version 221 | ;; 222 | suse) 223 | if [ -f /etc/os-release ]; then 224 | cat /etc/os-release 225 | else 226 | cat /etc/SuSE-release 227 | fi 228 | ;; 229 | ubuntu) 230 | lsb_release -sr 231 | ;; 232 | esac 233 | 234 | } 235 | 236 | function _uptime() { 237 | 238 | if command -v uptime >/dev/null; then 239 | uptime | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/) {d=$6;h=$8;m=$9} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes"}' 240 | fi 241 | 242 | } 243 | 244 | function _last_reboot() { 245 | 246 | if command -v last >/dev/null; then 247 | last reboot -F | head -1 | awk '{print $6,$7,$8,$9}' 248 | fi 249 | 250 | } 251 | 252 | function _cpu_cores() { 253 | 254 | os=$(_os) 255 | case "$os" in 256 | "macosx") 257 | sysctl -n hw.physicalcpu 258 | ;; 259 | "openbsd") 260 | sysctl -n hw.ncpuonline 261 | ;; 262 | *) 263 | if [ -r /proc/cpuinfo ]; then 264 | cores="$(grep "core id" /proc/cpuinfo | sort | uniq | wc -l)" 265 | if [ "${cores}" -eq 0 ]; then 266 | cores="1" 267 | fi 268 | echo "$cores" 269 | fi 270 | ;; 271 | esac 272 | 273 | } 274 | 275 | function _cpu_sockets() { 276 | 277 | os=$(_os) 278 | case "$os" in 279 | "macosx") 280 | system_profiler SPHardwareDataType | grep "Number of Processors" | awk -F': ' '{print $2}' 281 | ;; 282 | *) 283 | if [ -r /proc/cpuinfo ]; then 284 | sockets="$(grep "physical id" /proc/cpuinfo | sort -u | wc -l)" 285 | if [ "${sockets}" -eq 0 ]; then 286 | sockets="$(grep -c "processor" /proc/cpuinfo)" 287 | fi 288 | echo "${sockets}" 289 | fi 290 | ;; 291 | esac 292 | 293 | } 294 | 295 | function _hostname() { 296 | 297 | if command -v hostname >/dev/null; then 298 | hostname 299 | else 300 | uname -n 301 | fi 302 | 303 | } 304 | 305 | function _kernel_name() { 306 | 307 | uname -s 308 | 309 | } 310 | 311 | function _machine() { 312 | 313 | if command -v uname >/dev/null 2>&1 ; then 314 | uname -m 315 | fi 316 | 317 | } 318 | 319 | function _machine_type() { 320 | 321 | if [ -d "/proc/vz" ] && [ ! -d "/proc/bc" ]; then 322 | echo openvz 323 | exit 324 | fi 325 | 326 | if [ -e "/proc/1/environ" ] && 327 | tr '\000' '\n' < "/proc/1/environ" | grep -Eiq '^container='; then 328 | echo lxc 329 | exit 330 | fi 331 | 332 | if [ -r /proc/cpuinfo ]; then 333 | # this should only exist on virtual guest machines, 334 | # tested on vmware, xen, kvm 335 | if grep -q "hypervisor" /proc/cpuinfo; then 336 | # this file is aviable in xen guest systems 337 | if [ -r /sys/hypervisor/type ]; then 338 | if grep -q -i "xen" /sys/hypervisor/type; then 339 | echo virtual_by_xen 340 | exit 341 | fi 342 | else 343 | if [ -r /sys/class/dmi/id/product_name ]; then 344 | if grep -q -i 'vmware' /sys/class/dmi/id/product_name; then 345 | echo "virtual_by_vmware" 346 | exit 347 | elif grep -q -i 'bochs' /sys/class/dmi/id/product_name; then 348 | echo "virtual_by_kvm" 349 | exit 350 | elif grep -q -i 'virtualbox' /sys/class/dmi/id/product_name; then 351 | echo "virtual_by_virtualbox" 352 | exit 353 | fi 354 | fi 355 | 356 | if [ -r /sys/class/dmi/id/sys_vendor ]; then 357 | if grep -q -i 'qemu' /sys/class/dmi/id/sys_vendor; then 358 | echo "virtual_by_kvm" 359 | exit 360 | fi 361 | fi 362 | 363 | if [ -r /sys/class/dmi/id/chassis_vendor ]; then 364 | if grep -q -i 'qemu' /sys/class/dmi/id/chassis_vendor; then 365 | echo "virtual_by_kvm" 366 | exit 367 | fi 368 | fi 369 | fi 370 | echo "virtual_by_unknown" 371 | else 372 | echo "physical" 373 | fi 374 | else 375 | echo "unknown" 376 | fi 377 | 378 | } 379 | 380 | function _memory() { 381 | 382 | os=$(_os) 383 | case "$os" in 384 | "macosx") 385 | echo "$(sysctl -n hw.memsize)/1024" | bc 386 | ;; 387 | 388 | "openbsd") 389 | echo "$(sysctl -n hw.physmem) / 1048576" | bc 390 | ;; 391 | 392 | *) 393 | if [ -r /proc/meminfo ]; then 394 | grep "MemTotal:" /proc/meminfo | awk '{print $2}' 395 | fi 396 | ;; 397 | esac 398 | 399 | } 400 | 401 | function _init() { 402 | 403 | uname_s="$(uname -s)" 404 | 405 | case "$uname_s" in 406 | Linux) 407 | (pgrep -P0 -l | awk '/^1[ \t]/ {print $2;}') || true 408 | ;; 409 | FreeBSD|OpenBSD) 410 | ps -o comm= -p 1 || true 411 | ;; 412 | *) 413 | # return a empty string as unknown value 414 | echo "" 415 | ;; 416 | esac 417 | 418 | } 419 | 420 | function _lsb_codename() { 421 | 422 | set +e 423 | case "$(_os)" in 424 | openwrt) 425 | (. /etc/openwrt_release && echo "$DISTRIB_CODENAME") 426 | ;; 427 | *) 428 | lsb_release=$(command -v lsb_release) 429 | if [ -x "$lsb_release" ]; then 430 | $lsb_release --short --codename 431 | fi 432 | ;; 433 | esac 434 | 435 | } 436 | 437 | function _lsb_description() { 438 | 439 | set +e 440 | case "$(_os)" in 441 | openwrt) 442 | (. /etc/openwrt_release && echo "$DISTRIB_DESCRIPTION") 443 | ;; 444 | *) 445 | lsb_release=$(command -v lsb_release) 446 | if [ -x "$lsb_release" ]; then 447 | $lsb_release --short --description 448 | fi 449 | ;; 450 | esac 451 | 452 | } 453 | 454 | function _lsb_id() { 455 | 456 | set +e 457 | case "$(_os)" in 458 | openwrt) 459 | (. /etc/openwrt_release && echo "$DISTRIB_ID") 460 | ;; 461 | *) 462 | lsb_release=$(command -v lsb_release) 463 | if [ -x "$lsb_release" ]; then 464 | $lsb_release --short --id 465 | fi 466 | ;; 467 | esac 468 | 469 | } 470 | 471 | function _lsb_release() { 472 | 473 | set +e 474 | case "$(_os)" in 475 | openwrt) 476 | (. /etc/openwrt_release && echo "$DISTRIB_RELEASE") 477 | ;; 478 | *) 479 | lsb_release=$(command -v lsb_release) 480 | if [ -x "$lsb_release" ]; then 481 | $lsb_release --short --release 482 | fi 483 | ;; 484 | esac 485 | 486 | } 487 | 488 | function _runlevel() { 489 | 490 | set +e 491 | executable=$(command -v runlevel) 492 | if [ -x "$executable" ]; then 493 | "$executable" | awk '{ print $2 }' 494 | fi 495 | 496 | } 497 | 498 | function _disks() { 499 | 500 | uname_s="$(uname -s)" 501 | 502 | case "$uname_s" in 503 | FreeBSD) 504 | sysctl -n kern.disks 505 | ;; 506 | OpenBSD|NetBSD) 507 | sysctl -n hw.disknames | grep -Eo '[lsw]d[0-9]+' | xargs 508 | ;; 509 | Linux) 510 | if command -v lsblk > /dev/null; then 511 | # exclude ram disks, floppies and cdroms 512 | # https://www.kernel.org/doc/Documentation/admin-guide/devices.txt 513 | lsblk -e 1,2,11 -dno name | xargs 514 | else 515 | printf "Don't know how to list disks for %s operating system without lsblk, if you can please submit a patch\n" "${uname_s}" >&2 516 | fi 517 | ;; 518 | *) 519 | printf "Don't know how to list disks for %s operating system, if you can please submit a patch\n" "${uname_s}" >&2 520 | ;; 521 | esac 522 | 523 | } 524 | 525 | function get_facts() { 526 | 527 | local function_name=${1} 528 | local label=$( echo "${function_name/_/}" | tr '[:lower:]' '[:upper:]' ) 529 | local fact="$( ${function_name} )" 530 | 531 | [[ -n "${fact// }" ]] && echo "${label}=${fact}" 532 | 533 | } 534 | 535 | get_facts _os 536 | get_facts _os_version 537 | get_facts _uptime 538 | get_facts _last_reboot 539 | get_facts _cpu_cores 540 | get_facts _cpu_sockets 541 | get_facts _hostname 542 | get_facts _kernel_name 543 | get_facts _machine 544 | get_facts _machine_type 545 | get_facts _memory 546 | get_facts _init 547 | get_facts _lsb_codename 548 | get_facts _lsb_description 549 | get_facts _lsb_id 550 | get_facts _lsb_release 551 | get_facts _runlevel 552 | get_facts _disks 553 | 554 | END 555 | -------------------------------------------------------------------------------- /ssh-force-password: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +---------------------------------------------------------------------------------------------------------------+ 4 | # | Title : ssh-force-password | 5 | # | Description : Enforces password authentication (as long as the server allows it) | 6 | # | It became quite annoying googling the SSH options for this every time | 7 | # | Author : Sven Wick | 8 | # | URL : https://github.com/vaporup/ssh-tools | 9 | # | Based On : https://www.cyberciti.biz/faq/howto-force-ssh-client-login-to-use-only-password-authentication | 10 | # +---------------------------------------------------------------------------------------------------------------+ 11 | 12 | # shellcheck disable=SC2029 13 | 14 | ssh_opts=( 15 | -o "PreferredAuthentications=password" 16 | -o "PubkeyAuthentication=no" 17 | ) 18 | 19 | # 20 | # Usage/Help message 21 | # 22 | 23 | function usage() { 24 | 25 | cat << EOF 26 | 27 | Usage: ${0##*/} [DEFAULT SSH OPTIONS] hostname 28 | 29 | Enforces password authentication (for password testing) 30 | 31 | EOF 32 | 33 | } 34 | 35 | if [[ -z $1 || $1 == "--help" ]]; then 36 | usage 37 | exit 1 38 | fi 39 | 40 | ssh "${ssh_opts[@]}" "$@" 41 | -------------------------------------------------------------------------------- /ssh-hostkeys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +-----------------------------------------------------------------------------------------------+ 4 | # | Title : ssh-hostkeys | 5 | # | Description : Prints server host keys in several formats | 6 | # | Author : Sven Wick | 7 | # | Contributors : Geert Stappers | 8 | # | URL : https://github.com/vaporup/ssh-tools | 9 | # | Based On : https://unix.stackexchange.com/questions/126908/get-ssh-server-key-fingerprint | 10 | # +-----------------------------------------------------------------------------------------------+ 11 | 12 | # shellcheck disable=SC2207 13 | 14 | # 15 | # Usage/Help message 16 | # 17 | 18 | function usage() { 19 | 20 | cat << EOF 21 | 22 | Usage: ${0##*/} [OPTIONS] hostname 23 | 24 | OPTIONS: 25 | -4 Use IPv4 only 26 | -6 Use IPv6 only 27 | -h Show this message 28 | -T timeout Time to wait for a response, in seconds 29 | -p port Port to connect to on the remote host. 30 | 31 | EOF 32 | 33 | } 34 | 35 | if [[ -z $1 || $1 == "--help" ]]; then 36 | usage 37 | exit 1 38 | fi 39 | 40 | # 41 | # Command line Options 42 | # 43 | 44 | SSH_FLAGS=() 45 | 46 | # shellcheck disable=SC2249 47 | while getopts ":46hp:T:" opt; do 48 | case ${opt} in 49 | 4 ) 50 | SSH_FLAGS+=("-4") 51 | ;; 52 | 6 ) 53 | SSH_FLAGS+=("-6") 54 | ;; 55 | h ) 56 | usage 57 | exit 1 58 | ;; 59 | p ) 60 | [[ ${OPTARG} =~ ^[0-9]+$ ]] && SSH_FLAGS+=("-p") && SSH_FLAGS+=("${OPTARG}") 61 | ;; 62 | T ) 63 | SSH_FLAGS+=("-T") && SSH_FLAGS+=("${OPTARG}") 64 | ;; 65 | \? ) 66 | echo "Invalid option: ${OPTARG}" 1>&2 67 | usage 68 | exit 1 69 | ;; 70 | esac 71 | done 72 | 73 | shift $((OPTIND - 1)) 74 | 75 | remote_host=$1 76 | 77 | the_hostkeys=$( mktemp /tmp/ssh-hostkeys.XXXXXX ) 78 | trap 'rm -f $the_hostkeys' EXIT 79 | 80 | ssh-keyscan "${SSH_FLAGS[@]}" "${remote_host}" > "${the_hostkeys}" 2>/dev/null 81 | 82 | fingerprint_hashes=( md5 sha256 ) 83 | 84 | function get_fingerprints () { 85 | 86 | hash_type=$1 87 | 88 | ssh-keygen -E "${hash_type}" -qlf "${the_hostkeys}" | while IFS= read -r line; do 89 | 90 | key_data=( $(printf '%s\n' "${line}") ) 91 | key_size=${key_data[0]} 92 | key_hash=${key_data[1]} 93 | #key_remote_host=${key_data[2]} 94 | key_type=${key_data[3]} 95 | key_hash_type="${key_hash%%:*}" 96 | key_hash_data="${key_hash#*:}" 97 | 98 | printf "%10s%6s%8s %s\n" "${key_type}" "${key_size}" "${key_hash_type}" "${key_hash_data}" 99 | 100 | done 101 | 102 | } 103 | 104 | for fingerprint_hash in "${fingerprint_hashes[@]}"; do 105 | 106 | get_fingerprints "${fingerprint_hash}" 107 | 108 | done | sort 109 | -------------------------------------------------------------------------------- /ssh-keyinfo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +----------------------------------------------------+ 4 | # | Title : ssh-keyinfo | 5 | # | Description : Prints keys in several formats | 6 | # | Author : Sven Wick | 7 | # | URL : https://github.com/vaporup/ssh-tools | 8 | # +----------------------------------------------------+ 9 | 10 | # shellcheck disable=SC2207 11 | 12 | # 13 | # Usage/Help message 14 | # 15 | 16 | function usage() { 17 | 18 | cat << EOF 19 | 20 | Usage: ${0##*/} FILE [...] 21 | 22 | Examples: 23 | 24 | ${0##*/} ~/.ssh/id_rsa.pub 25 | 26 | ${0##*/} ~/.ssh/*.pub 27 | 28 | EOF 29 | 30 | } 31 | 32 | if [[ -z $1 || $1 == "--help" ]]; then 33 | usage 34 | exit 1 35 | fi 36 | 37 | fingerprint_hashes=( md5 sha256 ) 38 | 39 | function get_fingerprints () { 40 | 41 | hash_type=$1 42 | key_file=$2 43 | 44 | ssh-keygen -E "${hash_type}" -qlf "${key_file}" | while IFS= read -r line; do 45 | 46 | key_data=( $(printf '%s\n' "${line}") ) 47 | key_size=${key_data[0]} 48 | key_hash=${key_data[1]} 49 | #key_comment=${key_data[2]} 50 | key_type=${key_data[-1]} 51 | key_hash_type="${key_hash%%:*}" 52 | key_hash_data="${key_hash#*:}" 53 | 54 | printf "%10s%6s%8s %-50s %s\n" "${key_type}" "${key_size}" "${key_hash_type}" "${key_hash_data}" "${key_file}" 55 | 56 | done 57 | 58 | } 59 | 60 | KEYS=( "$@" ) 61 | 62 | for KEY in "${KEYS[@]}"; do 63 | 64 | for fingerprint_hash in "${fingerprint_hashes[@]}"; do 65 | get_fingerprints "${fingerprint_hash}" "${KEY}" 66 | done 67 | 68 | done 69 | -------------------------------------------------------------------------------- /ssh-last: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # +-----------------------------------------------------+ 4 | # | Title : ssh-last | 5 | # | Description : like last but for SSH sessions | 6 | # | Author : Sven Wick | 7 | # | URL : https://github.com/vaporup/ssh-tools | 8 | # +-----------------------------------------------------+ 9 | 10 | # Die Implementierung dieser Software beruht ganz oder in Teilen 11 | # auf Konzepten und Ideen, die mit freundlicher Genehmigung der Firma AB+M GmbH 12 | # mit Sitz in Karlsruhe (Deutschland) dem firmeninternen Python-Skript ssh_last entnommen wurden. 13 | 14 | # The implementation of this software is based in whole 15 | # or in part on concepts and ideas taken from a Python script called ssh_last, 16 | # by courtesy of the company AB+M GmbH, based in Karlsruhe (Germany). 17 | 18 | # ************************************************************************** 19 | # 20 | # "You may sometimes find that others have made 21 | # more of your ideas than you have yourself" 22 | # 23 | # -- Tim O'Reilly 24 | # 25 | # http://radar.oreilly.com/2009/01/work-on-stuff-that-matters-fir.html 26 | # 27 | # ************************************************************************** 28 | 29 | # Divergence to the original: 30 | # =========================== 31 | # 32 | # 1) Perl instead of Python 33 | # 34 | # 2) Works on a broad set of operating systems not only the latest Ubuntu 35 | # - Major GNU/Linux distributions 36 | # - Niche GNU/Linux distributions like guix, void, etc... 37 | # - Major BSD distributions like OpenBSD, FreeBSD, DragonFlyBSD, 38 | # works even on Firewall appliances like pfSense and OPNSense 39 | # 40 | # 3) Uses systemd's journal by default and logfiles only as a fallback 41 | # when run on Non-Linux or Non-systemd Linux systems 42 | # and you can even pipe in the logs. 43 | # 44 | # 4) The algorithm to reconstruct SSH sessions works differently: 45 | # 46 | # Some distributions, especially some BSDs, do different privilege separation 47 | # which results in changing the PID during login phase for a normal user connection 48 | # 49 | # https://security.stackexchange.com/questions/115896/can-someone-explain-how-sshd-does-privilege-separation 50 | # 51 | # Relying upon PIDs is therefore not very robust 52 | # because you can't map login and logout session afterwards anymore 53 | # so a different algorithm using TCP ports was used instead 54 | # (the port stays the same during the whole session including login phase) 55 | # 56 | # Also, finding the logout timestamp is implemented in a more precise fashion: 57 | # 58 | # The original version fetches just the last log of a PID 59 | # which sometimes creates a false positive 60 | # when a SSH session was not closed cleanly and the PID was recycled for another SSH session. 61 | # 62 | # This implementation therefore uses a counting algorithm 63 | # to piece together a SSH session by its Accepted and Disconnected logline 64 | # 65 | # More info: https://utcc.utoronto.ca/~cks/space/blog/linux/OpenSSHDisconnectLogging 66 | # 67 | # 5) The "Known" and "Ignored" mechanics are implemented 68 | # in a hierarchical filesystem manner instead of hardcoding them into the script 69 | # 70 | # 6) The output includes some flags which give a hint about which auth type was used: 71 | # 72 | # e.g: 73 | # 74 | # (C) sshd authorized login via (c)ertificate 75 | # (K) sshd authorized login via public (k)ey 76 | # (?) sshd authorized login via some other type (password, pam) 77 | 78 | use strict; 79 | use warnings; 80 | 81 | use Memoize; 82 | use Pod::Usage; 83 | use Getopt::Std; 84 | use Data::Dumper; 85 | use Time::Piece; 86 | use Time::Seconds; 87 | use File::Basename; 88 | use Term::ANSIColor; 89 | 90 | $Data::Dumper::Sortkeys = 1; 91 | 92 | # +-------+ 93 | # | USAGE | 94 | # +-------+ 95 | 96 | sub print_usage { 97 | pod2usage(); 98 | return; 99 | } 100 | 101 | sub print_usage_full { 102 | pod2usage(-verbose => 2); 103 | return; 104 | } 105 | 106 | if ( defined($ARGV[0]) ) { 107 | 108 | if ( $ARGV[0] eq '-h' or $ARGV[0] eq '--help' ) { 109 | 110 | &print_usage; 111 | exit; 112 | 113 | } 114 | 115 | if ( $ARGV[0] eq '-?' ) { 116 | 117 | &print_usage_full; 118 | exit; 119 | 120 | } 121 | } 122 | 123 | # +---------+ 124 | # | OPTIONS | 125 | # +---------+ 126 | 127 | my %opts; 128 | 129 | my $show_all = 0; 130 | my $colors = 0; 131 | my $debug = 0; 132 | my $show_fingerprints = 0; 133 | my $show_cert_ids = 0; 134 | my $show_host_in_clear = 0; 135 | my $who_mode = 0; 136 | my $use_logfiles = 0; 137 | 138 | getopts('acdfilnw', \%opts); 139 | 140 | $show_all = 1 if $opts{a}; 141 | $colors = 1 if $opts{c}; 142 | $debug = 1 if $opts{d}; 143 | $show_fingerprints = 1 if $opts{f}; 144 | $show_cert_ids = 1 if $opts{i}; 145 | $show_host_in_clear = 1 if $opts{n}; 146 | $who_mode = 1 if $opts{w}; 147 | $use_logfiles = 1 if $opts{l}; 148 | 149 | # +---------+ 150 | # | REGEXES | 151 | # +---------+ 152 | # 153 | # (default) 154 | # 155 | # These very likely change further down the code for different operating systems 156 | 157 | my $matches_login = '^(?\w+\s+\d+\s+\d+:\d+:\d+)' 158 | . '\s+(?\S+)' 159 | . '\s+sshd\[(?\d+)\]:' 160 | . '\s+Accepted\s+(?\S+)' 161 | . '\s+for\s+(?\S+)' 162 | . '\s+from\s+(?\S+)' 163 | . '\s+port\s+(?\d+)\s+ssh2:?\s*(?
.*)' 164 | ; 165 | 166 | my $matches_logout = '^(?\w+\s+\d+\s+\d+:\d+:\d+)' 167 | . '\s+(?\S+)' 168 | . '\s+sshd\[(?\d+)\]:' 169 | . '\s+Disconnected' 170 | . '\s+from\s+user\s+(?\S+)' 171 | . '\s+(?\S+)' 172 | . '\s+port\s+(?\d+)' 173 | ; 174 | 175 | my $matches_cert = '^(?\S+-CERT) (?\S+)' 176 | . ' ID (?\S+)' 177 | ; 178 | 179 | my $matches_key = '^(?\w+) (?\S+)' 180 | ; 181 | 182 | # +-----------------+ 183 | # | DATA STRUCTURES | 184 | # +-----------------+ 185 | 186 | my %session_ids_counter; # Self-created IDs 187 | # from network port and count of its occurence 188 | # in chronological order from logfiles 189 | # 190 | # Example with Port 1234 191 | # 192 | # - First occurrence in log -> ID = 1234-1 193 | # - Second occurrence in log -> ID = 1234-2 194 | 195 | my @ssh_session_ids; # List to store Login IDs in chronological order 196 | my %ssh_sessions; # Hash to store all the data we parse from logs 197 | 198 | # +------------------------------------------+ 199 | # | SUBS FOR DATE AND TIME STRING FORMATTING | 200 | # +------------------------------------------+ 201 | 202 | sub str2epoch { 203 | 204 | my (@args) = @_; 205 | my $str = $args[0]; 206 | #my $now = localtime; 207 | my $now = Time::Piece->new(); 208 | my $year = $now->year; 209 | 210 | # YEAR is missing in syslog timestamp, so add the current year. 211 | # 212 | # Example: "Aug 23 00:22:05" -> "2022 Aug 23 00:22:05" 213 | 214 | $str =~ s/^/$year /g; 215 | 216 | # Parse Timestamp string 217 | # 218 | # Force localtime instead UTC 219 | # https://stackoverflow.com/a/47722347 220 | 221 | my $t = localtime->strptime($str, '%Y %b %d %H:%M:%S'); 222 | 223 | # If the timestamp is in the future, it is from last year. 224 | 225 | if ( $t > $now ) { 226 | 227 | $t = $t - ONE_YEAR; 228 | } 229 | 230 | return $t->epoch; 231 | } 232 | 233 | sub str2epoch_opensuse { 234 | 235 | my (@args) = @_; 236 | my $str = $args[0]; 237 | my $now = Time::Piece->new(); 238 | 239 | # Parse Timestamp string 240 | # 241 | # Force localtime instead UTC 242 | # https://stackoverflow.com/a/47722347 243 | 244 | my $t = localtime->strptime($str, '%Y-%m-%d %H:%M:%S'); 245 | 246 | return $t->epoch; 247 | } 248 | 249 | sub format_seconds { 250 | 251 | # 1 year = 31557600 seconds 252 | 253 | my (@args) = @_; 254 | 255 | my $total_seconds = $args[0]; 256 | 257 | my ($hours, $hourremainder) = (($total_seconds/(60*60)), $total_seconds % (60*60)); 258 | my ($minutes, $seconds) = (int $hourremainder / 60, $hourremainder % 60); 259 | 260 | ($hours, $minutes, $seconds) = (sprintf('%02d', $hours), sprintf('%02d', $minutes), sprintf('%02d', $seconds)); 261 | 262 | return $hours . ':' . $minutes . ':' . $seconds; 263 | } 264 | 265 | # +-------------------+ 266 | # | ignored and known | 267 | # +-------------------+ 268 | 269 | my $matches_data = '\s*(?\S+)\s*(?.*)'; 270 | 271 | my @ignored_files = ( 272 | '/etc/ssh-tools/ssh-last/ignored', 273 | glob('~/.config/ssh-tools/ssh-last/ignored'), 274 | 'ignored', 275 | ); 276 | 277 | my @known_files = ( 278 | '/etc/ssh-tools/ssh-last/known', 279 | glob('~/.config/ssh-tools/ssh-last/known'), 280 | 'known', 281 | ); 282 | 283 | sub get_file_data { 284 | 285 | my $data_type = $_[0]; 286 | 287 | my %file_data; 288 | my @data_files; 289 | 290 | if ( $data_type eq 'ignored' ) { 291 | @data_files = @ignored_files; 292 | } 293 | 294 | if ( $data_type eq 'known' ) { 295 | @data_files = @known_files; 296 | } 297 | 298 | foreach my $data_file (@data_files) { 299 | 300 | if ( -r $data_file ) { 301 | 302 | open( my $file_fh, '<', $data_file ); 303 | 304 | while ( my $line = <$file_fh> ) { 305 | 306 | chomp($line); 307 | 308 | if ($line =~ /^\s*#.*/ ) { next; } # Ignore comments 309 | 310 | if ($line =~ /$matches_data/ ) { 311 | 312 | my $key = $+{KEY}; 313 | my $value = $+{VALUE}; 314 | 315 | $value =~ s/#+.*$//g; # Remove comment from rest of the line 316 | $value =~ s/\s+$//g; # Remove whitespace from rest of the line 317 | 318 | $file_data{$key} = $value; 319 | } 320 | } 321 | } 322 | } 323 | 324 | return %file_data; 325 | }; 326 | 327 | my %ignored = get_file_data('ignored'); 328 | my %known = get_file_data('known'); 329 | 330 | # +--------------------------------------------------------------+ 331 | # | SUBS FOR MAPPING A FINGERPRINT IN .ssh/authorized_keys | 332 | # | TO ITS COMMENT FIELD (using ssh-keygen -lf) | 333 | # | TO SHOW THE COMMENT IN THE OUTPUT INSTEAD OF THE FINGERPRINT | 334 | # +--------------------------------------------------------------+ 335 | 336 | sub get_ssh_keygen_data { 337 | 338 | my $fh = $_[0]; 339 | my @lines = `ssh-keygen -lf $fh`; 340 | return @lines; 341 | 342 | } 343 | 344 | # Caching the result, so the data does not need to be generated again and again. 345 | # Saves further unnecessarily calls of ssh-keygen. 346 | 347 | memoize('get_ssh_keygen_data'); 348 | 349 | sub detail_from_fingerprint { 350 | 351 | my $user = $_[0]; 352 | my $fp = $_[1]; 353 | 354 | if ($known{$fp}) { 355 | return $known{$fp}; 356 | } 357 | 358 | if ($ignored{$fp}) { 359 | return $ignored{$fp}; 360 | } 361 | 362 | # In scalar context, glob iterates through such filename expansions, 363 | # returning undef when the list is exhausted. 364 | # 365 | # So, we use a list first 366 | # 367 | # https://stackoverflow.com/questions/1274642/why-does-perls-glob-return-undef-for-every-other-call 368 | 369 | my @authorized_keys_files = glob("~${user}/.ssh/authorized_keys"); 370 | my $authorized_keys_file = $authorized_keys_files[0]; 371 | 372 | if ( $authorized_keys_file ) { 373 | 374 | if ( -r $authorized_keys_file ) { 375 | 376 | my @lines = get_ssh_keygen_data($authorized_keys_file); 377 | my $data = $fp; 378 | 379 | foreach my $line (@lines) { 380 | 381 | chomp $line; 382 | 383 | my @columns = split(' ', $line); 384 | 385 | my $fingerprint = $columns[1]; 386 | my $comment = $columns[2]; 387 | 388 | if ( $fp eq $fingerprint ) { 389 | 390 | $data = $comment; 391 | last; 392 | 393 | } 394 | } 395 | 396 | return $data; 397 | 398 | } 399 | else { 400 | return $fp; 401 | } 402 | 403 | } 404 | else { 405 | return $fp; 406 | } 407 | } 408 | 409 | # +--------------+ 410 | # | PARSING LOGS | 411 | # +--------------+ 412 | 413 | my $log_cmd_files_dragonfly = 'zgrep -hE "Accepted|Disconnected"' 414 | . ' /var/log/auth.log.6.gz' 415 | . ' /var/log/auth.log.5.gz' 416 | . ' /var/log/auth.log.4.gz' 417 | . ' /var/log/auth.log.3.gz' 418 | . ' /var/log/auth.log.2.gz' 419 | . ' /var/log/auth.log.1' 420 | . ' /var/log/auth.log' 421 | . ' 2>/dev/null'; 422 | 423 | my $log_cmd_files = 'zgrep -hE "Accepted|Disconnected"' 424 | . ' $(ls /var/log/auth.log* --sort=time --reverse)' 425 | . ' prevent-grep-to-wait' 426 | . ' 2>/dev/null'; 427 | 428 | my $log_cmd_files_secure = 'zgrep -hE "Accepted|Disconnected"' 429 | . ' $(ls /var/log/secure* --sort=time --reverse)' 430 | . ' prevent-grep-to-wait' 431 | . ' 2>/dev/null'; 432 | 433 | my $log_cmd_files_messages = 'zgrep -hE "Accepted|Disconnected"' 434 | . ' $(ls /var/log/messages* --sort=time --reverse)' 435 | . ' prevent-grep-to-wait' 436 | . ' 2>/dev/null'; 437 | 438 | my $log_cmd_files_alpine = 'grep -hE "Accepted|Disconnected"' 439 | . ' /var/log/messages.0' 440 | . ' /var/log/messages' 441 | . ' 2>/dev/null'; 442 | 443 | my $log_cmd_files_openbsd = 'zgrep -hE "Accepted|Disconnected"' 444 | . ' /var/log/authlog.6.gz' 445 | . ' /var/log/authlog.5.gz' 446 | . ' /var/log/authlog.4.gz' 447 | . ' /var/log/authlog.3.gz' 448 | . ' /var/log/authlog.2.gz' 449 | . ' /var/log/authlog.1.gz' 450 | . ' /var/log/authlog.0.gz' 451 | . ' /var/log/authlog' 452 | . ' 2>/dev/null'; 453 | 454 | my $log_cmd_files_freebsd = 'zgrep -hE "Accepted|Disconnected"' 455 | . ' /var/log/auth.log.6.bz2' 456 | . ' /var/log/auth.log.5.bz2' 457 | . ' /var/log/auth.log.4.bz2' 458 | . ' /var/log/auth.log.3.bz2' 459 | . ' /var/log/auth.log.2.bz2' 460 | . ' /var/log/auth.log.1.bz2' 461 | . ' /var/log/auth.log.0.bz2' 462 | . ' /var/log/auth.log' 463 | . ' 2>/dev/null'; 464 | 465 | my $log_cmd_files_pfsense = 'grep -hE "Accepted|Disconnected"' 466 | . ' /var/log/auth.log.6.bz2' 467 | . ' /var/log/auth.log.5.bz2' 468 | . ' /var/log/auth.log.4.bz2' 469 | . ' /var/log/auth.log.3.bz2' 470 | . ' /var/log/auth.log.2.bz2' 471 | . ' /var/log/auth.log.1.bz2' 472 | . ' /var/log/auth.log.0.bz2' 473 | . ' /var/log/auth.log' 474 | . ' 2>/dev/null'; 475 | 476 | my $log_cmd_files_opnsense = 'grep -hE "Accepted|Disconnected"' 477 | . ' /var/log/audit/latest.log' 478 | . ' 2>/dev/null'; 479 | 480 | my $log_cmd_journal = 'LC_TIME=C journalctl _COMM=sshd --no-pager -g "Accepted|Disconnected"'; 481 | my $log_cmd_journal_grep = 'LC_TIME=C journalctl _COMM=sshd --no-pager | grep -E "Accepted|Disconnected"'; 482 | 483 | # 484 | # Try first via journalctl 485 | # 486 | 487 | my $log_cmd = $log_cmd_journal; 488 | 489 | # 490 | # or via logfiles if requested by the user 491 | # 492 | 493 | if ( $use_logfiles ) { 494 | 495 | $log_cmd = $log_cmd_files; 496 | 497 | } 498 | 499 | # 500 | # Try different methods depending on operating system 501 | # 502 | 503 | my %os_data; 504 | 505 | $os_data{TYPE} = $^O; 506 | $os_data{ID} = '-'; 507 | 508 | if ( -e '/etc/pfSense-rc' ) { 509 | $os_data{ID} = 'pfsense'; 510 | } 511 | 512 | if ( -e '/usr/local/sbin/opnsense-shell' ) { 513 | $os_data{ID} = 'opnsense'; 514 | } 515 | 516 | if ( -r '/etc/os-release' ) { 517 | 518 | open( my $os_release_fh, '<', '/etc/os-release' ); 519 | 520 | while ( my $line = <$os_release_fh> ) { 521 | 522 | chomp($line); 523 | 524 | if ($line) { 525 | 526 | my @cols = split('=', $line) ; 527 | 528 | my $key = $cols[0]; 529 | my $value = $cols[1]; 530 | $value =~ s/"//g; # Remove all quotes: "10" -> 10 531 | 532 | $os_data{$key} = $value; 533 | } 534 | 535 | } 536 | } 537 | 538 | if ( $debug ) { 539 | print Dumper \%os_data; 540 | } 541 | 542 | if ( $os_data{TYPE} eq 'linux' ) { 543 | 544 | if ( $os_data{ID} eq 'debian' ) { 545 | 546 | # Debian Buster misses grep support in journalctl 547 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=890265 548 | 549 | if ( $os_data{VERSION_CODENAME} && $os_data{VERSION_CODENAME} eq 'buster' ) { 550 | $log_cmd = $log_cmd_journal_grep; 551 | $log_cmd = $log_cmd_files if $use_logfiles; 552 | } 553 | 554 | } 555 | 556 | if ( $os_data{ID} eq 'pureos' ) { 557 | 558 | # Misses grep support in journalctl 559 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=890265 560 | 561 | if ( $os_data{VERSION_CODENAME} eq 'amber' ) { 562 | $log_cmd = $log_cmd_journal_grep; 563 | $log_cmd = $log_cmd_files if $use_logfiles; 564 | } 565 | 566 | } 567 | 568 | if ( $os_data{ID} eq 'ubuntu' ) { 569 | 570 | if ( $os_data{VERSION_CODENAME} eq 'xenial' ) { 571 | 572 | # Ubuntu Xenial misses grep support in journalctl 573 | 574 | $log_cmd = $log_cmd_journal_grep; 575 | $log_cmd = $log_cmd_files if $use_logfiles; 576 | 577 | # Xenial logs Disconnects differently 578 | # 579 | # Normal: Disconnected from user root 192.168.1.101 port 48356 580 | # Xenial: Disconnected from 192.168.1.101 port 48356 581 | 582 | $matches_logout = '^(?\w+\s+\d+\s+\d+:\d+:\d+)' 583 | . '\s+(?\S+)' 584 | . '\s+sshd\[(?\d+)\]:' 585 | . '\s+Disconnected' 586 | . '\s+from' 587 | . '\s+(?\S+)' 588 | . '\s+port\s+(?\d+)' 589 | ; 590 | 591 | # Ubuntu Xenial does not log Fingerprint in CERT Details 592 | 593 | $matches_cert = '^(?\S+-CERT)' 594 | . ' ID (?\S+)' 595 | ; 596 | } 597 | 598 | if ( $os_data{VERSION_CODENAME} eq 'bionic' ) { 599 | 600 | # Ubuntu Bionic misses grep support in journalctl 601 | 602 | $log_cmd = $log_cmd_journal_grep; 603 | $log_cmd = $log_cmd_files if $use_logfiles; 604 | 605 | # Ubuntu Bionic does not log Fingerprint in CERT Details 606 | 607 | $matches_cert = '^(?\S+-CERT)' 608 | . ' ID (?\S+)' 609 | ; 610 | } 611 | 612 | } 613 | 614 | if ( $os_data{ID} eq 'mageia' ) { 615 | 616 | # Mageia 8 misses grep support in journalctl 617 | 618 | if ( $os_data{VERSION_ID} eq '8' ) { 619 | $log_cmd = $log_cmd_journal_grep; 620 | } 621 | 622 | } 623 | 624 | if ( $os_data{ID} eq 'opensuse-leap' ) { 625 | 626 | # OpenSUSE Leap 15.2 misses grep support in journalctl 627 | 628 | if ( $os_data{VERSION_ID} eq '15.2' ) { 629 | $log_cmd = $log_cmd_journal_grep; 630 | } 631 | 632 | # OpenSUSE Leap 15.4 grep support in journalctl does not work 633 | 634 | if ( $os_data{VERSION_ID} eq '15.4' ) { 635 | $log_cmd = $log_cmd_journal_grep; 636 | } 637 | 638 | if ( $use_logfiles ) { 639 | 640 | $log_cmd = $log_cmd_files_messages; 641 | 642 | $matches_login = '^(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})' 643 | . '\.(?\S+)' 644 | . '\s+(?\S+)' 645 | . '\s+sshd\[(?\d+)\]:' 646 | . '\s+Accepted\s+(?\S+)' 647 | . '\s+for\s+(?\S+)' 648 | . '\s+from\s+(?\S+)' 649 | . '\s+port\s+(?\d+)\s+ssh2:?\s*(?
.*)' 650 | ; 651 | 652 | $matches_logout = '^(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})' 653 | . '\.(?\S+)' 654 | . '\s+(?\S+)' 655 | . '\s+sshd\[(?\d+)\]:' 656 | . '\s+Disconnected' 657 | . '\s+from\s+user\s+(?\S+)' 658 | . '\s+(?\S+)' 659 | . '\s+port\s+(?\d+)' 660 | ; 661 | 662 | } 663 | 664 | } 665 | 666 | if ( $os_data{ID} eq 'alpine' ) { 667 | 668 | # Alpine uses OpenRC so we have to use logfiles 669 | 670 | $log_cmd = $log_cmd_files_alpine; 671 | 672 | $matches_login = '^(?\w+\s+\d+\s+\d+:\d+:\d+)' 673 | . '\s+(?\S+)' 674 | . '\s+(?\S+)' 675 | . '\s+sshd\[(?\d+)\]:' 676 | . '\s+Accepted\s+(?\S+)' 677 | . '\s+for\s+(?\S+)' 678 | . '\s+from\s+(?\S+)' 679 | . '\s+port\s+(?\d+)\s+ssh2:?\s*(?
.*)' 680 | ; 681 | 682 | $matches_logout = '^(?\w+\s+\d+\s+\d+:\d+:\d+)' 683 | . '\s+(?\S+)' 684 | . '\s+(?\S+)' 685 | . '\s+sshd\[(?\d+)\]:' 686 | . '\s+Disconnected' 687 | . '\s+from\s+user\s+(?\S+)' 688 | . '\s+(?\S+)' 689 | . '\s+port\s+(?\d+)' 690 | ; 691 | 692 | } 693 | 694 | if ( $os_data{ID} eq 'devuan' ) { 695 | 696 | # Devuan uses other Init systems so we have to use logfiles 697 | 698 | $log_cmd = $log_cmd_files; 699 | 700 | if ( $os_data{PRETTY_NAME} =~ /ascii\s*$/ ) { 701 | 702 | # Devuan 2 ASCII logs Disconnects differently 703 | # 704 | # Normal: Disconnected from user root 192.168.1.101 port 48356 705 | # Devuan: Disconnected from 192.168.1.101 port 48356 706 | 707 | $matches_logout = '^(?\w+\s+\d+\s+\d+:\d+:\d+)' 708 | . '\s+(?\S+)' 709 | . '\s+sshd\[(?\d+)\]:' 710 | . '\s+Disconnected' 711 | . '\s+from' 712 | . '\s+(?\S+)' 713 | . '\s+port\s+(?\d+)' 714 | ; 715 | 716 | # Devuan 2 ASCII does not log Fingerprint in CERT Details 717 | 718 | $matches_cert = '^(?\S+-CERT)' 719 | . ' ID (?\S+)' 720 | ; 721 | } 722 | } 723 | 724 | if ( $os_data{ID} eq 'trisquel' ) { 725 | 726 | # Misses grep support in journalctl 727 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=890265 728 | 729 | if ( $os_data{VERSION_CODENAME} && $os_data{VERSION_CODENAME} eq 'etiona' ) { 730 | $log_cmd = $log_cmd_journal_grep; 731 | $log_cmd = $log_cmd_files if $use_logfiles; 732 | } 733 | 734 | } 735 | 736 | if ( $os_data{ID_LIKE} && $os_data{ID_LIKE} =~ /rhel|centos|fedora/ ) { 737 | 738 | $log_cmd = $log_cmd_files_secure if $use_logfiles; 739 | 740 | } 741 | 742 | if ( $os_data{ID} eq 'void' ) { 743 | 744 | $log_cmd = $log_cmd_files_messages; 745 | 746 | $matches_login = '^(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})' 747 | . '\.(?\S+)' 748 | . '\s+(?\S+)' 749 | . '\s+sshd\[(?\d+)\]:' 750 | . '\s+Accepted\s+(?\S+)' 751 | . '\s+for\s+(?\S+)' 752 | . '\s+from\s+(?\S+)' 753 | . '\s+port\s+(?\d+)\s+ssh2:?\s*(?
.*)' 754 | ; 755 | 756 | $matches_logout = '^(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})' 757 | . '\.(?\S+)' 758 | . '\s+(?\S+)' 759 | . '\s+sshd\[(?\d+)\]:' 760 | . '\s+Disconnected' 761 | . '\s+from\s+user\s+(?\S+)' 762 | . '\s+(?\S+)' 763 | . '\s+port\s+(?\d+)' 764 | ; 765 | 766 | } 767 | 768 | if ( $os_data{ID} eq 'slackware' ) { 769 | 770 | $log_cmd = $log_cmd_files_messages; 771 | 772 | } 773 | 774 | if ( $os_data{ID} eq 'guix' ) { 775 | 776 | $log_cmd = $log_cmd_files_messages; 777 | 778 | } 779 | 780 | } 781 | 782 | if ( $os_data{TYPE} eq 'openbsd' ) { 783 | 784 | $log_cmd = $log_cmd_files_openbsd; 785 | } 786 | 787 | if ( $os_data{TYPE} eq 'freebsd' ) { 788 | 789 | $log_cmd = $log_cmd_files_freebsd; 790 | 791 | if ( $os_data{ID} && $os_data{ID} eq 'pfsense' ) { 792 | 793 | # pfSense announces itself as freebsd 794 | # but ships with an old zgrep version 795 | # that ignores -h and still shows filenames 796 | 797 | $log_cmd = $log_cmd_files_pfsense; 798 | 799 | } 800 | 801 | if ( $os_data{ID} && $os_data{ID} eq 'opnsense' ) { 802 | 803 | # pfSense announces itself as freebsd 804 | 805 | $log_cmd = $log_cmd_files_opnsense; 806 | 807 | $matches_login = '^<.*>\d+\s*(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})' 808 | . '\+(?\S+)' 809 | . '\s+(?\S+)' 810 | . '\s+sshd (?\d+)' 811 | . '\s+\-\s+\[meta.*\]' 812 | . '\s+Accepted\s+(?\S+)' 813 | . '\s+for\s+(?\S+)' 814 | . '\s+from\s+(?\S+)' 815 | . '\s+port\s+(?\d+)\s+ssh2:?\s*(?
.*)' 816 | ; 817 | 818 | $matches_logout = '^<.*>\d+\s*(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})' 819 | . '\+(?\S+)' 820 | . '\s+(?\S+)' 821 | . '\s+sshd (?\d+)' 822 | . '\s+\-\s+\[meta.*\]' 823 | . '\s+Disconnected' 824 | . '\s+from\s+user\s+(?\S+)' 825 | . '\s+(?\S+)' 826 | . '\s+port\s+(?\d+)' 827 | ; 828 | 829 | } 830 | 831 | } 832 | 833 | if ( $os_data{TYPE} eq 'dragonfly' ) { 834 | 835 | $log_cmd = $log_cmd_files_dragonfly; 836 | 837 | } 838 | 839 | my $logs_h; 840 | 841 | # If ssh-last was called from a terminal, get data via log_cmd. 842 | # If data comes via STDIN pipe, get it from there. 843 | 844 | if ( -t STDIN ) { 845 | open( $logs_h, '-|', $log_cmd); 846 | } 847 | else { 848 | $logs_h = *STDIN; 849 | } 850 | 851 | my $log_count = 0; 852 | 853 | while (my $log = <$logs_h>) { 854 | 855 | print STDERR "Parsing log entries... ($log_count)\r"; 856 | $log_count = $log_count + 1; 857 | 858 | chomp($log); 859 | 860 | if ( $log =~ /$matches_login/ ) { 861 | 862 | my $timestamp = $+{TS} ; 863 | my $hostname = $+{HOSTNAME} ; 864 | my $pid = $+{PID} ; 865 | my $user = $+{USER} ; 866 | my $host = $+{HOST} ; 867 | my $port = $+{PORT} ; 868 | my $auth_type = $+{AUTH_TYPE} ; 869 | my $details = $+{DETAILS} ; 870 | 871 | if ( $os_data{ID} eq 'opensuse-leap' && $use_logfiles ) { 872 | $timestamp =~ s/T/ /g; 873 | } 874 | 875 | if ( $os_data{ID} eq 'void' ) { 876 | $timestamp =~ s/T/ /g; 877 | } 878 | 879 | if ( $os_data{ID} eq 'opnsense' ) { 880 | $timestamp =~ s/T/ /g; 881 | } 882 | 883 | $session_ids_counter{$port}{login_count}++; 884 | 885 | my $session_id = $port . '-' . $session_ids_counter{$port}{login_count}; 886 | 887 | push @ssh_session_ids, $session_id; 888 | 889 | $ssh_sessions{$session_id}{login} = $timestamp ; 890 | $ssh_sessions{$session_id}{logout} = '-' ; 891 | $ssh_sessions{$session_id}{hostname} = $hostname ; 892 | $ssh_sessions{$session_id}{pid} = $pid ; 893 | $ssh_sessions{$session_id}{user} = $user ; 894 | $ssh_sessions{$session_id}{host} = $host ; 895 | $ssh_sessions{$session_id}{port} = $port ; 896 | $ssh_sessions{$session_id}{auth_type} = $auth_type ; 897 | $ssh_sessions{$session_id}{details} = $details ; 898 | $ssh_sessions{$session_id}{auth_id} = '-' ; 899 | $ssh_sessions{$session_id}{session_id} = $session_id; 900 | 901 | unless ( $show_host_in_clear ) { 902 | 903 | if ( $known{$host} ) { 904 | $ssh_sessions{$session_id}{host} = $known{$host} ; 905 | 906 | } 907 | 908 | } 909 | 910 | if ( $auth_type eq 'publickey' ) { 911 | $ssh_sessions{$session_id}{auth_id} = '(-) ' . $auth_type ; 912 | } 913 | else { 914 | $ssh_sessions{$session_id}{auth_id} = '(?) ' . $auth_type ; 915 | } 916 | 917 | if ( $debug ) { 918 | print "\n"; 919 | print('> LOG: ' , "$log" , "\n"); 920 | print('> TS: ' , "$timestamp" , "\n"); 921 | print('> HOSTNAME: ' , "$hostname" , "\n"); 922 | print('> PID: ' , "$pid" , "\n"); 923 | print('> USER: ' , "$user" , "\n"); 924 | print('> HOST: ' , "$host" , "\n"); 925 | print('> PORT: ' , "$port" , "\n"); 926 | print('> AUTH_TYPE: ' , "$auth_type" , "\n"); 927 | print('> DETAILS: ' , "$details" , "\n"); 928 | print('> SESSION_ID: ' , "$session_id" , "\n"); 929 | } 930 | 931 | # Set flag to ignore unwanted hosts in output 932 | 933 | if ( exists $ignored{$host}) { 934 | $ssh_sessions{$session_id}{ignore} = 'true'; 935 | } 936 | 937 | my $known_host = $known{$host}; 938 | 939 | if ( $known_host && exists $ignored{$known_host}) { 940 | $ssh_sessions{$session_id}{ignore} = 'true'; 941 | } 942 | 943 | # Set flag to ignore unwanted users in output 944 | 945 | if ( exists $ignored{$user}) { 946 | $ssh_sessions{$session_id}{ignore} = 'true'; 947 | } 948 | 949 | if ( $details ) { 950 | 951 | if ( $details =~ /$matches_key/ ) { 952 | 953 | my $key_type = $+{KEY_TYPE} ; 954 | my $fingerprint = $+{FINGERPRINT} ; 955 | 956 | # Set flag to ignore unwanted fingerprints in output 957 | 958 | if ( exists $ignored{$fingerprint}) { 959 | $ssh_sessions{$session_id}{ignore} = 'true'; 960 | } 961 | 962 | $ssh_sessions{$session_id}{key_type} = $key_type; 963 | 964 | if ( $show_fingerprints ) { 965 | $ssh_sessions{$session_id}{auth_id} = '(K) ' . $fingerprint; 966 | } 967 | else { 968 | $ssh_sessions{$session_id}{auth_id} = '(K) ' . detail_from_fingerprint($user ,$fingerprint); 969 | } 970 | 971 | if ( $debug ) { 972 | print('> DETAILS: ' , 'KEY_FOUND' , "\n"); 973 | print('> KEY_TYPE: ' , "$key_type" , "\n"); 974 | print('> FINGERPRINT: ' , "$fingerprint" , "\n"); 975 | } 976 | 977 | } 978 | 979 | if ( $details =~ /$matches_cert/ ) { 980 | 981 | my $key_type = $+{KEY_TYPE} ; 982 | my $fingerprint = $+{FINGERPRINT} ; 983 | my $auth_id = $+{AUTH_ID} ; 984 | 985 | unless ( $fingerprint ) { 986 | $fingerprint = '-'; 987 | } 988 | 989 | # Set flag to ignore unwanted fingerprints in output 990 | 991 | if ( exists $ignored{$fingerprint}) { 992 | $ssh_sessions{$session_id}{ignore} = 'true'; 993 | } 994 | 995 | # Set flag to ignore unwanted cert ids in output 996 | 997 | if ( exists $ignored{$auth_id}) { 998 | $ssh_sessions{$session_id}{ignore} = 'true'; 999 | } 1000 | 1001 | $ssh_sessions{$session_id}{key_type} = $key_type; 1002 | 1003 | if ( $show_fingerprints ) { 1004 | $ssh_sessions{$session_id}{auth_id} = '(C) ' . $fingerprint; 1005 | } 1006 | else { 1007 | 1008 | if ( $known{$auth_id} ) { 1009 | 1010 | if ( $show_cert_ids ) { 1011 | $ssh_sessions{$session_id}{auth_id} = '(C) ' . $auth_id; 1012 | } 1013 | else { 1014 | 1015 | $ssh_sessions{$session_id}{auth_id} = '(C) ' . $known{$auth_id} ; 1016 | } 1017 | } 1018 | else { 1019 | $ssh_sessions{$session_id}{auth_id} = '(C) ' . $auth_id; 1020 | } 1021 | } 1022 | 1023 | if ( $debug ) { 1024 | print('> DETAILS: ' , 'CERT_FOUND' , "\n"); 1025 | print('> KEY_TYPE: ' , "$key_type" , "\n"); 1026 | print('> FINGERPRINT: ' , "$fingerprint" , "\n"); 1027 | print('> AUTH_ID: ' , "$auth_id" , "\n"); 1028 | } 1029 | 1030 | } 1031 | 1032 | } 1033 | } 1034 | 1035 | if ( $log =~ /$matches_logout/ ) { 1036 | 1037 | my $timestamp = $+{TS} ; 1038 | my $hostname = $+{HOSTNAME} ; 1039 | my $user = $+{USER} ; 1040 | my $pid = $+{PID} ; 1041 | my $host = $+{HOST} ; 1042 | my $port = $+{PORT} ; 1043 | 1044 | if ( $os_data{ID} eq 'opensuse-leap' && $use_logfiles ) { 1045 | $timestamp =~ s/T/ /g; 1046 | } 1047 | 1048 | if ( $os_data{ID} eq 'void' ) { 1049 | $timestamp =~ s/T/ /g; 1050 | } 1051 | 1052 | if ( $os_data{ID} eq 'opnsense' ) { 1053 | $timestamp =~ s/T/ /g; 1054 | } 1055 | 1056 | my $session_id; 1057 | 1058 | if ( $session_ids_counter{$port}{login_count} ) { 1059 | 1060 | $session_id = $port . '-' . $session_ids_counter{$port}{login_count}; 1061 | } 1062 | else { 1063 | # If there is no previous login 1064 | # to this port in log, skip this logline. 1065 | # 1066 | # Example: Login line was rolled over by logration 1067 | # and "Disconnect" is still in log 1068 | # but the "Accepted" entry was discarded. 1069 | next; 1070 | } 1071 | 1072 | $ssh_sessions{$session_id}{logout} = $timestamp; 1073 | 1074 | if ( $debug ) { 1075 | 1076 | print "\n"; 1077 | print("< $log", "\n"); 1078 | 1079 | unless ( $user ) { 1080 | $user = '-'; 1081 | } 1082 | 1083 | print('< TS: ' , "$timestamp" , "\n"); 1084 | print('< HOSTNAME: ' , "$hostname" , "\n"); 1085 | print('< PID: ' , "$pid" , "\n"); 1086 | print('< USER: ' , "$user" , "\n"); 1087 | print('< HOST: ' , "$host" , "\n"); 1088 | print('< PORT: ' , "$port" , "\n"); 1089 | print('< SESSION_ID: ' , "$session_id" , "\n"); 1090 | } 1091 | } 1092 | 1093 | } 1094 | 1095 | # +----------------+ 1096 | # | PRINT SESSIONS | 1097 | # +----------------+ 1098 | 1099 | my $output_format = "%-15s %-15s %-10s %-15s %-15s %-5s %-15s\n"; 1100 | 1101 | if ( $os_data{ID} && $os_data{ID} eq 'opensuse-leap' && $use_logfiles ) { 1102 | 1103 | $output_format = "%-20s %-20s %-10s %-15s %-15s %-5s %-15s\n"; 1104 | } 1105 | 1106 | if ( $os_data{ID} && $os_data{ID} eq 'void' ) { 1107 | 1108 | $output_format = "%-20s %-20s %-10s %-15s %-15s %-5s %-15s\n"; 1109 | } 1110 | 1111 | if ( $os_data{ID} && $os_data{ID} eq 'opnsense' ) { 1112 | 1113 | $output_format = "%-20s %-20s %-10s %-15s %-15s %-5s %-15s\n"; 1114 | } 1115 | 1116 | print "\n"; 1117 | 1118 | if ( $debug ) { 1119 | print Dumper \%ssh_sessions; 1120 | } 1121 | 1122 | # 1123 | # HEADER 1124 | # 1125 | 1126 | if ( $colors ) { 1127 | 1128 | print color 'bold white'; 1129 | 1130 | printf( $output_format, 1131 | 'LOGIN', 1132 | 'LOGOUT', 1133 | 'DURATION', 1134 | 'USER', 1135 | 'HOST', 1136 | 'PORT', 1137 | 'AUTH_ID', 1138 | ); 1139 | 1140 | print color 'reset'; 1141 | 1142 | } 1143 | else { 1144 | 1145 | printf( $output_format, 1146 | 'LOGIN', 1147 | 'LOGOUT', 1148 | 'DURATION', 1149 | 'USER', 1150 | 'HOST', 1151 | 'PORT', 1152 | 'AUTH_ID', 1153 | ); 1154 | 1155 | } 1156 | 1157 | # 1158 | # SESSIONS 1159 | # 1160 | 1161 | foreach my $session ( @ssh_session_ids ) { 1162 | 1163 | # 1164 | # ignore unwanted data in output 1165 | # 1166 | 1167 | if ( $ssh_sessions{$session}{ignore} ) { 1168 | 1169 | # 1170 | # but not if user insists on seeing them (-a) 1171 | # 1172 | 1173 | unless ( $show_all ) { 1174 | next; 1175 | } 1176 | } 1177 | 1178 | my $port = $ssh_sessions{$session}{port}; 1179 | 1180 | if ( $ssh_sessions{$session}{logout} eq '-' ) { 1181 | 1182 | $ssh_sessions{$session}{duration} = '-'; 1183 | 1184 | my $command = "LANG=C ss -tnp state ESTABLISHED | grep -q :\"${port}\\s*users.*sshd\""; 1185 | 1186 | if ( $os_data{TYPE} eq 'openbsd' ) { 1187 | $command = "LANG=C fstat | grep -q \"sshd.*internet.*tcp.*:${port}\""; 1188 | } 1189 | 1190 | if ( $os_data{TYPE} eq 'freebsd' ) { 1191 | $command = "LANG=C sockstat -c | grep -q \"sshd.*tcp.*:${port}\""; 1192 | } 1193 | 1194 | if ( $os_data{TYPE} eq 'dragonfly' ) { 1195 | $command = "LANG=C sockstat -c | grep -q \"sshd.*tcp.*:${port}\""; 1196 | } 1197 | 1198 | if ( $os_data{TYPE} eq 'linux' ) { 1199 | if ( $os_data{ID} eq 'alpine' ) { 1200 | $command = "LANG=C netstat -n -t | grep -qE \":${port} +ESTABLISHED\""; 1201 | } 1202 | } 1203 | 1204 | my $error = system $command; 1205 | 1206 | if ( $error ) { 1207 | # That is actually not an error, 1208 | # it just means that grep did not find 1209 | # a TCP Session with that port 1210 | # so we can assume the user is not logged in anymore 1211 | } 1212 | else { 1213 | $ssh_sessions{$session}{logout} = 'still logged in'; 1214 | 1215 | my $login_epoch; 1216 | 1217 | if ( $os_data{ID} && $os_data{ID} eq 'opensuse-leap' && $use_logfiles ) { 1218 | $login_epoch = str2epoch_opensuse($ssh_sessions{$session}{login}); 1219 | } 1220 | elsif ( $os_data{ID} && $os_data{ID} eq 'void' ) { 1221 | $login_epoch = str2epoch_opensuse($ssh_sessions{$session}{login}); 1222 | } 1223 | elsif ( $os_data{ID} && $os_data{ID} eq 'opnsense' ) { 1224 | $login_epoch = str2epoch_opensuse($ssh_sessions{$session}{login}); 1225 | } 1226 | else { 1227 | $login_epoch = str2epoch($ssh_sessions{$session}{login}); 1228 | } 1229 | 1230 | my $current_time = time(); 1231 | my $duration = $current_time - $login_epoch; 1232 | 1233 | $ssh_sessions{$session}{duration} = format_seconds($duration); 1234 | } 1235 | } 1236 | else { 1237 | my $login_epoch; 1238 | my $logout_epoch; 1239 | 1240 | if ( $os_data{ID} && $os_data{ID} eq 'opensuse-leap' && $use_logfiles ) { 1241 | $login_epoch = str2epoch_opensuse($ssh_sessions{$session}{login}); 1242 | $logout_epoch = str2epoch_opensuse($ssh_sessions{$session}{logout}); 1243 | } 1244 | elsif ( $os_data{ID} eq 'void' ) { 1245 | $login_epoch = str2epoch_opensuse($ssh_sessions{$session}{login}); 1246 | $logout_epoch = str2epoch_opensuse($ssh_sessions{$session}{logout}); 1247 | } 1248 | elsif ( $os_data{ID} eq 'opnsense' ) { 1249 | $login_epoch = str2epoch_opensuse($ssh_sessions{$session}{login}); 1250 | $logout_epoch = str2epoch_opensuse($ssh_sessions{$session}{logout}); 1251 | } 1252 | else { 1253 | $login_epoch = str2epoch($ssh_sessions{$session}{login}); 1254 | $logout_epoch = str2epoch($ssh_sessions{$session}{logout}); 1255 | } 1256 | 1257 | my $duration = $logout_epoch - $login_epoch; 1258 | 1259 | $ssh_sessions{$session}{duration} = format_seconds($duration); 1260 | } 1261 | 1262 | if ( $ssh_sessions{$session}{logout} =~ /still/ ) { 1263 | 1264 | if ( $colors ) { 1265 | print color 'bright_cyan'; 1266 | 1267 | printf( $output_format, 1268 | $ssh_sessions{$session}{login}, 1269 | $ssh_sessions{$session}{logout}, 1270 | $ssh_sessions{$session}{duration}, 1271 | $ssh_sessions{$session}{user}, 1272 | $ssh_sessions{$session}{host}, 1273 | $ssh_sessions{$session}{port}, 1274 | $ssh_sessions{$session}{auth_id}, 1275 | ); 1276 | 1277 | print color 'reset'; 1278 | 1279 | } 1280 | else { 1281 | 1282 | printf( $output_format, 1283 | $ssh_sessions{$session}{login}, 1284 | $ssh_sessions{$session}{logout}, 1285 | $ssh_sessions{$session}{duration}, 1286 | $ssh_sessions{$session}{user}, 1287 | $ssh_sessions{$session}{host}, 1288 | $ssh_sessions{$session}{port}, 1289 | $ssh_sessions{$session}{auth_id}, 1290 | ); 1291 | 1292 | } 1293 | 1294 | } 1295 | else { 1296 | 1297 | if ( $who_mode ) { 1298 | next; 1299 | } 1300 | else { 1301 | printf( $output_format, 1302 | $ssh_sessions{$session}{login}, 1303 | $ssh_sessions{$session}{logout}, 1304 | $ssh_sessions{$session}{duration}, 1305 | $ssh_sessions{$session}{user}, 1306 | $ssh_sessions{$session}{host}, 1307 | $ssh_sessions{$session}{port}, 1308 | $ssh_sessions{$session}{auth_id}, 1309 | ); 1310 | } 1311 | 1312 | } 1313 | 1314 | } 1315 | 1316 | __END__ 1317 | 1318 | =head1 NAME 1319 | 1320 | ssh-last - list last SSH sessions 1321 | 1322 | =head1 SYNOPSIS 1323 | 1324 | ssh-last [OPTIONS] 1325 | ssh_logs | ssh-last [OPTIONS] 1326 | 1327 | =head2 Options 1328 | 1329 | -a show all sessions (show data which is hidden by the 'ignored' file) 1330 | -c colored output (highlight active SSH sessions) 1331 | -d debug 1332 | -f force showing fingerprints (no mapping from 'known' file) 1333 | -h show this help message 1334 | -i force showing certificate ids (no mapping from 'known' file, not together with -f) 1335 | -l try to use logfiles instead of journalctl (may be even faster on some systems) 1336 | -n show host/ip in cleartext (no mapping from 'known' file) 1337 | -w show only active SSH sessions 1338 | -? show complete manual with more detailed information 1339 | (usually needs perl-doc installed to work properly) 1340 | 1341 | =head2 Examples 1342 | 1343 | ssh-last 1344 | ssh-last -c | more 1345 | ssh-last -c | less -R # keeps colored output in less 1346 | ssh-last -cw 1347 | 1348 | # Logs from yesterday 1349 | LC_TIME=C journalctl _COMM=sshd -g 'Accepted|Disconnected' --since yesterday | ssh-last 1350 | 1351 | # Logs from three days ago 1352 | LC_TIME=C journalctl _COMM=sshd -g 'Accepted|Disconnected' --since -3d --until -2d | ssh-last 1353 | 1354 | # Logs from the last hour 1355 | LC_TIME=C journalctl _COMM=sshd -g 'Accepted|Disconnected' --since -1h | ssh-last 1356 | 1357 | # Logs until a specific date 1358 | LC_TIME=C journalctl _COMM=sshd -g 'Accepted|Disconnected' --until "2022-03-12 07:00:00" | ssh-last 1359 | 1360 | # From logfiles (order must be from oldest to newest) 1361 | zgrep -hE 'Accepted|Disconnected' auth.log.2.gz auth.log.1 auth.log | ssh-last 1362 | zgrep -hE 'Accepted|Disconnected' $(ls /var/log/auth.log* --sort=time --reverse) | ssh-last 1363 | zgrep -hE 'Accepted|Disconnected' $(ls /var/log/messages* --sort=time --reverse) | ssh-last 1364 | zgrep -hE 'Accepted|Disconnected' $(ls /var/log/secure* --sort=time --reverse) | ssh-last 1365 | 1366 | =head1 DESCRIPTION 1367 | 1368 | ssh-last is like last but for SSH sessions 1369 | 1370 | =head2 Output Flags 1371 | 1372 | +--------------------------------------------------------------------------+ 1373 | | | 1374 | | AUTH_ID | 1375 | | | 1376 | | (C) sshd authorized login via (c)ertificate | 1377 | | (K) sshd authorized login via public (k)ey | 1378 | | (?) sshd authorized login via some other type (password, pam) | 1379 | | | 1380 | +--------------------------------------------------------------------------+ 1381 | 1382 | =head2 Algorithm 1383 | 1384 | Milling through sshd logs in chronological order: 1385 | 1386 | 1) Finding login (Accepted) and logout (Disconnected) lines. 1387 | 2) Storing info from the lines like username, auth_type, fingerprint, ... 1388 | 3) Using the used network port to check for active sessions 1389 | and piecing together old sessions by remembering logged network ports 1390 | 4) Using mainly /etc/os-release to adapt for different systems 1391 | which differ in logfile names, logging patterns, etc... 1392 | 1393 | =head1 FILES 1394 | 1395 | =head2 Ignored 1396 | 1397 | /etc/ssh-tools/ssh-last/ignored 1398 | ~/.config/ssh-tools/ssh-last/ignored 1399 | ./ignored 1400 | 1401 | These data will be hidden in output unless forced with -a option 1402 | 1403 | +--------------------------------------------------------------------------+ 1404 | |# Fingerprints | 1405 | | | 1406 | |SHA256:ElgyEn5xPe4VlK5jJkqauRdAKNRHdh2tGHfo0m9/IwW Jenkins | 1407 | |SHA256:5xPe4JkqaElKNRHGHfxPe4RdAKdh2tlK5AKNRHn5xK5 foo # comment | 1408 | |SHA256:nmKL5s7/fs45312nvjhFSRTREa44r2hfgJHJG54353R bar@gmx.de | 1409 | | | 1410 | |# Hosts | 1411 | | | 1412 | |127.0.0.1 localhost # local ssh logins | 1413 | |192.168.1.50 nas # more comments | 1414 | |webserver # alias from the 'known' file | 1415 | | | 1416 | |# Cert IDs | 1417 | | | 1418 | |user1@company.com | 1419 | |user2@company.com with some info | 1420 | |user3@company.com with some info # and a comment | 1421 | | | 1422 | |# Users | 1423 | | | 1424 | |git # gitlab | 1425 | +--------------------------------------------------------------------------+ 1426 | 1427 | =head2 Known 1428 | 1429 | /etc/ssh-tools/ssh-last/known 1430 | ~/.config/ssh-tools/ssh-last/known 1431 | ./known 1432 | 1433 | For these keys the mapped value will be shown instead of its key, 1434 | unless forced with -f (fingerprints) and -n (hosts) 1435 | or -i (certificate ids) option 1436 | 1437 | +--------------------------------------------------------------------------+ 1438 | |# Fingerprints | 1439 | | | 1440 | |SHA256:WwI/9m0ofHGt2hdHRNKAdRuaqkJj5KlV4ePx5nEyglE Sven Wick | 1441 | |SHA256:xyk5ZZZWZKnmKL5mYdk8Poy5eds7/CD/JEwqykMnlQQ root@n40l # comment | 1442 | |SHA256:G7h9i5+NDU72Ae40gCkxyvDz/8BH+KETw7sXHCYr5w0 sven.wick@gmx.de | 1443 | | | 1444 | |# Hosts | 1445 | | | 1446 | |127.0.0.1 localhost # local ssh logins | 1447 | |192.168.1.50 nas # more comments | 1448 | |192.168.50.100 webserver | 1449 | | | 1450 | |# Cert IDs | 1451 | | | 1452 | |user1@company.com vaporup | 1453 | +--------------------------------------------------------------------------+ 1454 | 1455 | =head1 BUGS AND LIMITATIONS 1456 | 1457 | =head2 JumpHosts 1458 | 1459 | Using a JumpHost with ProxyCommand oder ProxyJump, 1460 | may often result in an unclean disconnect with nothing logged, 1461 | so LOGOUT and DURATION can not be displayed. 1462 | 1463 | =head2 Unprivileged users 1464 | 1465 | If possible, run ssh-last as root or via sudo 1466 | 1467 | 1) Logfiles and systemd's journal usually can't be read by a normal user 1468 | 2) ssh-last -w works only reliably as root, 1469 | since ss and netstat do not show process info when invoked as normal user 1470 | 3) ssh-last tries to map the fingerprint from a user's authorized_keys file 1471 | but users usually are not allowed to look into each others files 1472 | 1473 | =head2 OS Upgrades 1474 | 1475 | If you do an in-place upgrade like dist-upgrade on Debian/Ubuntu, 1476 | depending on the version difference, 1477 | it can happen that sshd logs differently from that point on 1478 | and you may have a mix of logs in new and old format 1479 | which results in ssh-last showing only the latest ones correctly 1480 | 1481 | =head1 NOTES 1482 | 1483 | =head2 Helper Scripts 1484 | 1485 | For convenience you can create little wrapper scripts like the following 1486 | which avoids parsing too many logs by limiting the data only to the last week 1487 | 1488 | my-ssh-last 1489 | +--------------------------------------------------------------------------+ 1490 | | #!/usr/bin/env bash | 1491 | | | 1492 | | LC_TIME=C journalctl _COMM=sshd --since -1week \ | 1493 | | | grep -E 'Accepted|Disconnected' \ | 1494 | | | ssh-last "$@" | 1495 | | | 1496 | +--------------------------------------------------------------------------+ 1497 | 1498 | =head1 SEE ALSO 1499 | 1500 | ssh-keyinfo(1), ssh-certinfo(1) 1501 | 1502 | =head1 AUTHOR 1503 | 1504 | Sven Wick 1505 | 1506 | =cut 1507 | -------------------------------------------------------------------------------- /ssh-ping: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +--------------------------------------------------------------------------------------+ 4 | # | Title : ssh-ping | 5 | # | Description : Check if host is reachable using ssh_config | 6 | # | Outputs 'Reply from' when server is reachable but login failed | 7 | # | Outputs 'Pong from' when server is reachable and login was successful | 8 | # | Author : Sven Wick | 9 | # | Contributors : Denis Meiswinkel | 10 | # | URL : https://github.com/vaporup/ssh-tools | 11 | # | Based On : https://unix.stackexchange.com/a/30146/247383 | 12 | # | https://stackoverflow.com/a/33277226 | 13 | # +--------------------------------------------------------------------------------------+ 14 | 15 | # trap CTRL-C and call print_statistics() 16 | trap print_statistics SIGINT 17 | 18 | # 19 | # Some colors for better output 20 | # 21 | 22 | RED='\033[0;91m' 23 | GREEN='\033[0;92m' 24 | YELLOW='\033[0;93m' 25 | BLUE='\033[0;94m' 26 | MAGENTA='\033[0;95m' 27 | CYAN='\033[0;96m' 28 | WHITE='\033[0;97m' 29 | BOLD='\033[1m' 30 | RESET='\033[0m' 31 | 32 | # 33 | # Default SSH Options 34 | # 35 | 36 | SSH_OPTS=( 37 | -o "BatchMode=yes" 38 | -o "CheckHostIP=no" 39 | -o "StrictHostKeyChecking=no" 40 | ) 41 | 42 | # 43 | # SSH Flags which get populated later 44 | # 45 | 46 | SSH_FLAGS=() 47 | 48 | # 49 | # Defaults 50 | # 51 | 52 | ping_count=0 # How many requests to do 53 | ping_interval=1 # Seconds to wait between sending each request 54 | connect_timeout=16 # Seconds to wait for a response 55 | ssh_seq=1 # Request Counter 56 | requests_transmitted=0 # Count how often we sent a request 57 | requests_received=0 # Count how often we got an answer 58 | requests_lost=0 # Count how often we lost an answer 59 | quiet="no" # Do not suppress output 60 | 61 | # 62 | # Usage/Help message 63 | # 64 | 65 | function usage() { 66 | 67 | cat << EOF 68 | 69 | Usage: ${0##*/} [OPTIONS] [user@]hostname 70 | 71 | OPTIONS: 72 | 73 | -4 Use IPv4 only 74 | -6 Use IPv6 only 75 | -c count Stop after sending request packets 76 | -C Connect as soon as the host responds 77 | and try reconnecting after a SSH session ends (e.g. rebooting). 78 | Useful also for IDRAC, IPMI, ILO devices, Switches, etc... 79 | which don't have a full shell environment. 80 | CTRL+C stops reconnect attempts. 81 | -F configfile Specifies an alternative per-user configuration file. 82 | If a configuration file is given on the command line, 83 | the system-wide configuration file ( /etc/ssh/ssh_config ) will be ignored. 84 | The default for the per-user configuration file is ~/.ssh/config. 85 | -h Show this message 86 | -i interval Wait seconds between sending each request. 87 | The default is 1 second. 88 | -l user Try login with as username. The default is the current value of \$USER. 89 | -D Print timestamp (unix time + microseconds as in gettimeofday) before each line 90 | -H Print timestamp (human readable) before each line 91 | -W timeout Time to wait for a response, in seconds 92 | -p port Port to connect to on the remote host. 93 | This can be specified on a per-host basis in the configuration file. 94 | -q Quiet output. 95 | Nothing is displayed except the summary lines at startup time and when finished 96 | -n No colors. 97 | (e.g. for black on white terminals) 98 | -v Verbose output 99 | 100 | ENVIRONMENT_VARIABLES: 101 | 102 | SSH_PING_NO_COLORS if set, no colors are shown (like -n) 103 | 104 | Example: SSH_PING_NO_COLORS=true ${0##*/} -c 1 hostname 105 | 106 | EXIT_CODES: 107 | 108 | 0 No requests lost 109 | 1 More than 1 request lost 110 | 2 All requests lost 111 | 112 | Example: ${0##*/} -q -c 1 hostname >/dev/null || ... 113 | 114 | 115 | EOF 116 | 117 | } 118 | 119 | if [[ -z $1 || $1 == "--help" ]]; then 120 | usage 121 | exit 1 122 | fi 123 | 124 | function command_exists() { 125 | 126 | command -v "${1}" >/dev/null 2>&1 127 | 128 | } 129 | 130 | function get_timestamp() { 131 | 132 | if [[ "${print_timestamp_human_readable}" == "yes" ]]; then 133 | date 134 | else 135 | if [[ "${OSTYPE}" == "linux-gnu" ]] && command_exists date; then 136 | date +%s.%6N 137 | else 138 | command_exists perl && perl -MTime::HiRes=time -e 'printf "%.6f", time' 139 | fi 140 | fi 141 | 142 | } 143 | 144 | function get_request_timestamp() { 145 | 146 | if [[ "${OSTYPE}" == "linux-gnu" ]] && command_exists date; then 147 | date +%s%3N 148 | else 149 | command_exists perl && perl -MTime::HiRes=time -e 'printf "%i", time * 1000' 150 | fi 151 | 152 | } 153 | 154 | function print_statistics() { 155 | 156 | [[ ${requests_transmitted} -eq 0 ]] && exit 157 | 158 | requests_loss=$(( 100 * requests_lost / requests_transmitted )) 159 | 160 | echo "" 161 | echo -e "${WHITE}---${RESET} ${YELLOW}${host}${RESET} ${WHITE}ping statistics${RESET} ${WHITE}---${RESET}" 162 | 163 | statistics_ok="${GREEN}${requests_transmitted}${RESET} ${WHITE}requests transmitted${RESET}, " 164 | statistics_ok+="${GREEN}${requests_received}${RESET} ${WHITE}requests received${RESET}, " 165 | statistics_ok+="${GREEN}${requests_loss}%${RESET} ${WHITE}request loss${RESET}" 166 | 167 | statistics_warn="${YELLOW}${requests_transmitted}${RESET} ${WHITE}requests transmitted${RESET}, " 168 | statistics_warn+="${YELLOW}${requests_received}${RESET} ${WHITE}requests received${RESET}, " 169 | statistics_warn+="${YELLOW}${requests_loss}%${RESET} ${WHITE}request loss${RESET}" 170 | 171 | statistics_crit="${RED}${requests_transmitted}${RESET} ${WHITE}requests transmitted${RESET}, " 172 | statistics_crit+="${RED}${requests_received}${RESET} ${WHITE}requests received${RESET}, " 173 | statistics_crit+="${RED}${requests_loss}%${RESET} ${WHITE}request loss${RESET}" 174 | 175 | [[ ${requests_loss} -eq 100 ]] && echo -e "${statistics_crit}" && exit 2 176 | [[ ${requests_loss} -gt 1 ]] && echo -e "${statistics_warn}" && exit 1 177 | [[ ${requests_loss} -eq 0 ]] && echo -e "${statistics_ok}" && exit 178 | 179 | } 180 | 181 | # 182 | # Command line Options 183 | # 184 | 185 | # shellcheck disable=SC2249 186 | while getopts ":46c:CF:hi:l:DHp:vW:qn" opt; do 187 | case ${opt} in 188 | 4 ) 189 | SSH_FLAGS+=("-4") 190 | ;; 191 | 6 ) 192 | SSH_FLAGS+=("-6") 193 | ;; 194 | c ) 195 | [[ ${OPTARG} =~ ^[0-9]+$ ]] && ping_count=${OPTARG} 196 | ;; 197 | C ) 198 | connect="yes" 199 | ;; 200 | F ) 201 | SSH_FLAGS+=("-F") 202 | SSH_FLAGS+=("${OPTARG}") 203 | ;; 204 | h ) 205 | usage 206 | exit 1 207 | ;; 208 | i ) 209 | ping_interval=${OPTARG} 210 | ;; 211 | l ) 212 | SSH_FLAGS+=("-l") && SSH_FLAGS+=("${OPTARG}") 213 | ;; 214 | D ) 215 | print_timestamp="yes" 216 | ;; 217 | H ) 218 | print_timestamp="yes" 219 | print_timestamp_human_readable="yes" 220 | ;; 221 | p ) 222 | [[ ${OPTARG} =~ ^[0-9]+$ ]] && SSH_FLAGS+=("-p") && SSH_FLAGS+=("${OPTARG}") 223 | ;; 224 | v ) 225 | verbose="yes" 226 | ;; 227 | W ) 228 | [[ ${OPTARG} =~ ^[0-9]+$ ]] && connect_timeout=${OPTARG} 229 | ;; 230 | q ) 231 | quiet="yes" 232 | ;; 233 | n ) 234 | colors="no" 235 | ;; 236 | \? ) 237 | echo "Invalid option: ${OPTARG}" 1>&2 238 | usage 239 | exit 1 240 | ;; 241 | esac 242 | done 243 | 244 | shift $((OPTIND - 1)) 245 | 246 | SSH_OPTS+=( -o "ConnectTimeout=${connect_timeout}" ) 247 | 248 | # 249 | # Getting username and host from command line without using grep and awk 250 | # 251 | # user@host -> user gets stored in $username 252 | # -> host gets stored in $host 253 | # 254 | # If no user@ was given on the command line 255 | # we just store the last argument as hostname 256 | # 257 | 258 | if [[ $1 == *"@"* ]]; then 259 | host="${1##*@}" 260 | username="${1%%@*}" 261 | else 262 | host=${1} 263 | fi 264 | 265 | # 266 | # Colors are counter productive 267 | # on black on white terminals 268 | # 269 | 270 | # shellcheck disable=SC2154 271 | if [[ -n "${SSH_PING_NO_COLORS}" ]]; then 272 | 273 | colors="no" 274 | 275 | fi 276 | 277 | if [[ ${colors} == no ]]; then 278 | 279 | unset -v RED GREEN YELLOW BLUE MAGENTA CYAN WHITE BOLD 280 | 281 | fi 282 | 283 | [[ -z "${host}" ]] && { echo -e "\n ${RED}Error: No target host given${RESET}" ; usage; exit 1; } 284 | 285 | # 286 | # Output header with optional debugging output 287 | # 288 | 289 | echo -e "${BOLD}SSHPING${RESET} ${YELLOW}${host}${RESET}" 290 | 291 | if [[ ${verbose} == yes ]]; then 292 | echo -e -n "${BLUE}" 293 | echo "SSH_FLAGS:" "${SSH_FLAGS[@]}" 294 | echo "SSH_OPTS:" "${SSH_OPTS[@]}" 295 | echo -e -n "${RESET}" 296 | fi 297 | 298 | if [[ ! "${OSTYPE}" == "linux-gnu" ]]; then 299 | command_exists perl || echo -e "${YELLOW}WARNING:${RESET} No perl found, time measure probably fails (${WHITE}time${RESET}=${RED}0${RESET} ms)" >&2 300 | fi 301 | 302 | while true; do 303 | 304 | # 305 | # ping only $count times or forever if $count = 0 306 | # 307 | 308 | [[ ${ping_count} -gt 0 ]] && [[ ${ssh_seq} -gt ${ping_count} ]] && break 309 | 310 | # 311 | # used for -D and or -H option 312 | # 313 | 314 | timestamp=$( get_timestamp ) 315 | 316 | # 317 | # Doing the actual request and measure its execution time 318 | # 319 | 320 | start_request=$( get_request_timestamp ) 321 | 322 | if [[ -z "${username}" ]]; then 323 | if [[ "${connect}" == "yes" ]]; then 324 | status=$(ssh "${SSH_FLAGS[@]}" "${SSH_OPTS[@]}" "sshping@${host}" echo pong 2>&1 | grep -oE 'pong|denied|sftp') 325 | else 326 | status=$(ssh "${SSH_FLAGS[@]}" "${SSH_OPTS[@]}" "${host}" echo pong 2>&1 | grep -oE 'pong|denied|sftp') 327 | fi 328 | else 329 | if [[ "${connect}" == "yes" ]]; then 330 | status=$(ssh "${SSH_FLAGS[@]}" "${SSH_OPTS[@]}" "sshping@${host}" echo pong 2>&1 | grep -oE 'pong|denied|sftp') 331 | else 332 | status=$(ssh "${SSH_FLAGS[@]}" "${SSH_OPTS[@]}" "${username}@${host}" echo pong 2>&1 | grep -oE 'pong|denied|sftp') 333 | fi 334 | fi 335 | 336 | end_request=$( get_request_timestamp ) 337 | time_request=$((end_request-start_request)) 338 | 339 | # 340 | # Output "Pong" if request succeeded by login in and echoing back our string 341 | # Output "Reply" if the SSH server is at least talking to us but login was denied 342 | # 343 | 344 | if [[ ${status} == pong ]]; then 345 | requests_received=$(( requests_received + 1 )) 346 | [[ ${quiet} == no ]] && [[ ${print_timestamp} == yes ]] && echo -e -n "${WHITE}[${RESET}${MAGENTA}${timestamp}${RESET}${WHITE}]${RESET} " 347 | [[ ${quiet} == no ]] && echo -e "${GREEN}Pong${RESET} ${WHITE}from${RESET} ${YELLOW}${host}${RESET}${WHITE}: ssh_seq${RESET}=${RED}${ssh_seq}${RESET} ${WHITE}time${RESET}=${RED}${time_request}${RESET} ms" 348 | elif [[ ${status} == denied || ${status} == sftp ]]; then 349 | requests_received=$(( requests_received + 1 )) 350 | [[ ${quiet} == no ]] && [[ ${print_timestamp} == yes ]] && echo -e -n "${WHITE}[${RESET}${MAGENTA}${timestamp}${RESET}${WHITE}]${RESET} " 351 | [[ ${quiet} == no ]] && echo -e "${CYAN}Reply${RESET} ${WHITE}from${RESET} ${YELLOW}${host}${RESET}${WHITE}: ssh_seq${RESET}=${RED}${ssh_seq}${RESET} ${WHITE}time${RESET}=${RED}${time_request}${RESET} ms" 352 | else 353 | requests_lost=$(( requests_lost + 1 )) 354 | fi 355 | 356 | requests_transmitted=${ssh_seq} 357 | ssh_seq=$(( ssh_seq + 1 )) 358 | 359 | if [[ "${connect}" == "yes" ]]; then 360 | if [[ -z "${username}" ]]; then 361 | ssh "${SSH_FLAGS[@]}" -o "BatchMode=no" "${SSH_OPTS[@]}" "${host}" 362 | else 363 | ssh "${SSH_FLAGS[@]}" -o "BatchMode=no" "${SSH_OPTS[@]}" "${username}@${host}" 364 | fi 365 | echo -e "Reconnecting... (Press CTRL+C to abort)" 366 | fi 367 | 368 | # 369 | # Don't sleep if we do just 1 request 370 | # 371 | 372 | [[ ${ping_count} -eq 1 ]] || sleep "${ping_interval}" 373 | 374 | done 375 | 376 | print_statistics 377 | -------------------------------------------------------------------------------- /ssh-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # +-----------------------------------------------------------------------------------------------------------+ 4 | # | Title : ssh-version | 5 | # | Description : Shows version of the SSH server you are connecting to | 6 | # | Author : Sven Wick | 7 | # | Contributors : Denis Meiswinkel | 8 | # | URL : https://github.com/vaporup/ssh-tools | 9 | # | Based On : http://www.commandlinefu.com/commands/view/1809/get-the-version-of-sshd-on-a-remote-system | 10 | # +-----------------------------------------------------------------------------------------------------------+ 11 | 12 | ssh_opts=( 13 | -o "BatchMode=yes" 14 | -o "CheckHostIP=no" 15 | -o "StrictHostKeyChecking=no" 16 | -o "ConnectTimeout=16" 17 | -o "PasswordAuthentication=no" 18 | -o "PubkeyAuthentication=no" 19 | ) 20 | 21 | # 22 | # Usage/Help message 23 | # 24 | 25 | function usage() { 26 | 27 | cat << EOF 28 | 29 | Usage: ${0##*/} [OPTIONS] hostname 30 | 31 | Examples: 32 | 33 | ${0##*/} 127.0.0.1 34 | 35 | ${0##*/} -p 35007 127.0.0.1 36 | 37 | 38 | EOF 39 | 40 | } 41 | 42 | if [[ -z $1 || $1 == "--help" ]]; then 43 | usage 44 | exit 1 45 | fi 46 | 47 | SSH_VERSION=$(ssh -vN "${ssh_opts[@]}" "$@" -l ssh-version 2>&1 | grep "remote software version") 48 | echo "${SSH_VERSION#debug1: }" 49 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | TEST_SSH_USER="root" 6 | TEST_SSH_SERVER="192.168.1.10" 7 | 8 | SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")" 9 | 10 | test_commands=( "shellcheck ${SCRIPT_PATH}/ssh-*" ) 11 | test_commands+=( "${SCRIPT_PATH}/ssh-keyinfo ${SCRIPT_PATH}/examples/*/*" ) 12 | test_commands+=( "${SCRIPT_PATH}/ssh-certinfo ${SCRIPT_PATH}/examples/*/*" ) 13 | test_commands+=( "${SCRIPT_PATH}/ssh-certinfo -v ${SCRIPT_PATH}/examples/*/*" ) 14 | test_commands+=( "${SCRIPT_PATH}/ssh-certinfo -c ${SCRIPT_PATH}/examples/*/*" ) 15 | test_commands+=( "${SCRIPT_PATH}/ssh-certinfo -cv ${SCRIPT_PATH}/examples/*/*" ) 16 | test_commands+=( "${SCRIPT_PATH}/ssh-certinfo -c -w 20000 ${SCRIPT_PATH}/examples/*/*" ) 17 | test_commands+=( "${SCRIPT_PATH}/ssh-certinfo -cv -w 20000 ${SCRIPT_PATH}/examples/*/*" ) 18 | test_commands+=( "CHECK_REMOTE_FILE_EXISTS=NO sshpass -e ${SCRIPT_PATH}/ssh-diff /etc/hosts ${TEST_SSH_USER}@${TEST_SSH_SERVER}" ) 19 | test_commands+=( "sshpass -e ${SCRIPT_PATH}/ssh-facts ${TEST_SSH_USER}@${TEST_SSH_SERVER}" ) 20 | test_commands+=( "${SCRIPT_PATH}/ssh-hostkeys ${TEST_SSH_SERVER}" ) 21 | test_commands+=( "sshpass -e ${SCRIPT_PATH}/ssh-ping -4 -v -c 3 -D ${TEST_SSH_USER}@${TEST_SSH_SERVER}" ) 22 | test_commands+=( "sshpass -e ${SCRIPT_PATH}/ssh-ping -4 -v -c 3 -H ${TEST_SSH_USER}@${TEST_SSH_SERVER}" ) 23 | test_commands+=( "SSH_PING_NO_COLORS=true sshpass -e ${SCRIPT_PATH}/ssh-ping -4 -v -c 3 -H ${TEST_SSH_USER}@${TEST_SSH_SERVER}" ) 24 | test_commands+=( "${SCRIPT_PATH}/ssh-version ${TEST_SSH_SERVER}" ) 25 | 26 | for (( i = 0; i < ${#test_commands[@]} ; i++ )); do 27 | printf "\n**** Running: ${test_commands[$i]} *****\n\n" 28 | 29 | # Run each command in array 30 | eval "${test_commands[$i]}" 31 | 32 | done 33 | 34 | printf "\n**** Finished *****\n\n" 35 | --------------------------------------------------------------------------------