├── .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 |
--------------------------------------------------------------------------------