panda-motd 0.0.11 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/bin/panda-motd +1 -1
- data/lib/panda_motd.rb +9 -4
- data/lib/panda_motd/component.rb +11 -2
- data/lib/panda_motd/component_error.rb +4 -1
- data/lib/panda_motd/components/ascii_text_art.rb +9 -7
- data/lib/panda_motd/components/fail_2_ban.rb +11 -9
- data/lib/panda_motd/components/filesystems.rb +33 -29
- data/lib/panda_motd/components/last_login.rb +43 -13
- data/lib/panda_motd/components/service_status.rb +35 -10
- data/lib/panda_motd/components/ssl_certificates.rb +64 -20
- data/lib/panda_motd/components/uptime.rb +22 -5
- data/lib/panda_motd/config.rb +25 -8
- data/lib/panda_motd/motd.rb +15 -2
- data/lib/panda_motd/version.rb +3 -1
- metadata +51 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5e27df79cdab81d15c44ff575075e876eb975b44
|
4
|
+
data.tar.gz: 954bf7eb0ff70fd62b86918dfe1cbd95c84d56bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af66d7e99d7396984468e5824b1902b1e595aea9022a27c1c128df98416f386c89f23f1c5415aca4da26045af458e4b6e5b5844111a3488aabb1e15270d79351
|
7
|
+
data.tar.gz: 306ec97a05b35bfb2a0418018fbfc1b627c0e69007bef88ac7932b29bb319418637f416ec2c3903de1ddfcd44f29e6abe413944b153ec85fc04669177ffbd4fc
|
data/bin/panda-motd
CHANGED
data/lib/panda_motd.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "require_all"
|
4
|
+
require_rel "panda_motd"
|
3
5
|
|
4
6
|
class PandaMOTD
|
7
|
+
# Creates a new MOTD instance, assuming a config file has been passed as an
|
8
|
+
# argument to the command.
|
5
9
|
def self.new_motd
|
6
10
|
if ARGV[0].nil?
|
7
|
-
puts
|
11
|
+
puts "You must provide a config file path as an argument to panda-motd."
|
8
12
|
else
|
9
13
|
MOTD.new(ARGV[0])
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
17
|
+
# Gets the root path of the gem.
|
13
18
|
def self.root
|
14
|
-
File.expand_path(
|
19
|
+
File.expand_path("..", __dir__)
|
15
20
|
end
|
16
21
|
end
|
data/lib/panda_motd/component.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Component
|
2
4
|
attr_reader :name, :errors, :results, :config
|
3
5
|
|
@@ -8,19 +10,26 @@ class Component
|
|
8
10
|
@errors = []
|
9
11
|
end
|
10
12
|
|
13
|
+
# Evaluates the component so that it has some meaningful output when it comes
|
14
|
+
# time to print the MOTD.
|
11
15
|
def process
|
12
16
|
raise NotImplementedError
|
13
17
|
end
|
14
18
|
|
19
|
+
# Gives the output of a component as a string.
|
15
20
|
def to_s
|
16
21
|
raise NotImplementedError
|
17
22
|
end
|
18
23
|
|
24
|
+
# The number of lines to print before the component in the context of the
|
25
|
+
# entire MOTD. 1 by default, if not configured.
|
19
26
|
def lines_before
|
20
|
-
@motd.config.component_config(@name)[
|
27
|
+
@motd.config.component_config(@name)["lines_before"] || 1
|
21
28
|
end
|
22
29
|
|
30
|
+
# The number of lines to print after the component in the context of the
|
31
|
+
# entire MOTD. 1 by default, if not configured.
|
23
32
|
def lines_after
|
24
|
-
@motd.config.component_config(@name)[
|
33
|
+
@motd.config.component_config(@name)["lines_after"] || 1
|
25
34
|
end
|
26
35
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
2
4
|
|
3
5
|
class ComponentError
|
4
6
|
attr_reader :component, :message
|
@@ -8,6 +10,7 @@ class ComponentError
|
|
8
10
|
@message = message
|
9
11
|
end
|
10
12
|
|
13
|
+
# Gets a printable error string in red.
|
11
14
|
def to_s
|
12
15
|
return "#{@component.name} error: ".red + @message.to_s
|
13
16
|
end
|
@@ -1,18 +1,20 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "artii"
|
4
|
+
require "colorize"
|
3
5
|
|
4
6
|
class ASCIITextArt < Component
|
5
7
|
def initialize(motd)
|
6
|
-
super(motd,
|
8
|
+
super(motd, "ascii_text_art")
|
7
9
|
end
|
8
10
|
|
9
11
|
def process
|
10
|
-
@text = `#{@config[
|
11
|
-
@art = Artii::Base.new font: @config[
|
12
|
+
@text = `#{@config["command"]}`
|
13
|
+
@art = Artii::Base.new font: @config["font"]
|
12
14
|
@results = @art.asciify(@text)
|
13
|
-
@results = @results.colorize(@config[
|
15
|
+
@results = @results.colorize(@config["color"].to_sym) if @config["color"]
|
14
16
|
rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
|
15
|
-
@errors << ComponentError.new(self,
|
17
|
+
@errors << ComponentError.new(self, "Invalid font name")
|
16
18
|
end
|
17
19
|
|
18
20
|
def to_s
|
@@ -1,31 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Fail2Ban < Component
|
2
4
|
def initialize(motd)
|
3
|
-
super(motd,
|
5
|
+
super(motd, "fail_2_ban")
|
4
6
|
end
|
5
7
|
|
6
8
|
def process
|
7
9
|
@results = {
|
8
|
-
jails: {}
|
10
|
+
jails: {},
|
9
11
|
}
|
10
12
|
|
11
|
-
@config[
|
13
|
+
@config["jails"].each do |jail|
|
12
14
|
status = jail_status(jail)
|
13
15
|
@results[:jails][jail] = {
|
14
16
|
total: status[:total],
|
15
|
-
current: status[:current]
|
17
|
+
current: status[:current],
|
16
18
|
}
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
22
|
def to_s
|
21
|
-
result = "Fail2Ban:\n"
|
23
|
+
result = +"Fail2Ban:\n"
|
22
24
|
@results[:jails].each do |name, stats|
|
23
|
-
result
|
24
|
-
result
|
25
|
-
result
|
25
|
+
result << " #{name}:\n"
|
26
|
+
result << " Total bans: #{stats[:total]}\n"
|
27
|
+
result << " Current bans: #{stats[:current]}\n"
|
26
28
|
end
|
27
29
|
|
28
|
-
result.gsub(/\s$/,
|
30
|
+
result.gsub!(/\s$/, "")
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
@@ -1,13 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ruby-units"
|
4
|
+
require "colorize"
|
3
5
|
|
4
6
|
class Filesystems < Component
|
5
7
|
def initialize(motd)
|
6
|
-
super(motd,
|
8
|
+
super(motd, "filesystems")
|
7
9
|
end
|
8
10
|
|
9
11
|
def process
|
10
|
-
@results = parse_filesystem_usage(@config[
|
12
|
+
@results = parse_filesystem_usage(@config["filesystems"])
|
11
13
|
end
|
12
14
|
|
13
15
|
def to_s
|
@@ -17,11 +19,11 @@ class Filesystems < Component
|
|
17
19
|
|
18
20
|
size_w_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
|
19
21
|
|
20
|
-
result =
|
21
|
-
result
|
22
|
+
result = +"Filesystems".ljust(size_w_padding, " ")
|
23
|
+
result << "Size Used Free Use%\n"
|
22
24
|
|
23
25
|
@results.each do |filesystem|
|
24
|
-
result
|
26
|
+
result << format_filesystem(filesystem, size_w_padding)
|
25
27
|
end
|
26
28
|
|
27
29
|
result
|
@@ -33,30 +35,31 @@ class Filesystems < Component
|
|
33
35
|
return " #{filesystem}\n" if filesystem.is_a? String # handle fs not found
|
34
36
|
|
35
37
|
# filesystem name
|
36
|
-
result =
|
37
|
-
result
|
38
|
+
result = +""
|
39
|
+
result << " #{filesystem[:pretty_name]}".ljust(size, " ")
|
38
40
|
|
39
41
|
# statistics (size, used, free, use%)
|
40
42
|
[:size, :used, :avail].each do |metric|
|
41
|
-
result
|
43
|
+
result << format_metric(filesystem, metric)
|
42
44
|
end
|
45
|
+
|
43
46
|
percent_used = calc_percent_used(filesystem)
|
44
|
-
result
|
45
|
-
result
|
47
|
+
result << format_percent_used(percent_used)
|
48
|
+
result << "\n"
|
46
49
|
|
47
50
|
# visual bar representation of use%
|
48
|
-
result
|
51
|
+
result << generate_usage_bar(filesystem, size, percent_used)
|
49
52
|
|
50
53
|
result
|
51
54
|
end
|
52
55
|
|
53
56
|
def generate_usage_bar(filesystem, size, percent_used)
|
54
|
-
result =
|
57
|
+
result = +""
|
55
58
|
total_ticks = size + 18
|
56
59
|
used_ticks = (total_ticks * (percent_used.to_f / 100)).round
|
57
|
-
result
|
58
|
-
"#{(
|
59
|
-
result
|
60
|
+
result << " [#{("=" * used_ticks).send(pct_color(percent_used))}" \
|
61
|
+
"#{("=" * (total_ticks - used_ticks)).light_black}]"
|
62
|
+
result << "\n" unless filesystem == @results.last
|
60
63
|
result
|
61
64
|
end
|
62
65
|
|
@@ -74,7 +77,7 @@ class Filesystems < Component
|
|
74
77
|
end
|
75
78
|
|
76
79
|
def format_percent_used(percent_used)
|
77
|
-
(percent_used.to_s.rjust(3,
|
80
|
+
(percent_used.to_s.rjust(3, " ") + "%").send(pct_color(percent_used))
|
78
81
|
end
|
79
82
|
|
80
83
|
def calc_metric(value)
|
@@ -96,20 +99,21 @@ class Filesystems < Component
|
|
96
99
|
whole_number_length = value.scalar.floor.to_s.length
|
97
100
|
round_amount = whole_number_length > 1 ? 0 : 1
|
98
101
|
formatted = value.scalar.round(round_amount).to_s + value.units[0].upcase
|
99
|
-
|
102
|
+
|
103
|
+
formatted.rjust(4, " ") + " "
|
100
104
|
end
|
101
105
|
|
102
106
|
def calc_units(value)
|
103
|
-
if value > 10**12
|
104
|
-
|
105
|
-
elsif value > 10**9
|
106
|
-
|
107
|
-
elsif value > 10**6
|
108
|
-
|
109
|
-
elsif value > 10**3
|
110
|
-
|
107
|
+
if value > 10 ** 12
|
108
|
+
"terabytes"
|
109
|
+
elsif value > 10 ** 9
|
110
|
+
"gigabytes"
|
111
|
+
elsif value > 10 ** 6
|
112
|
+
"megabytes"
|
113
|
+
elsif value > 10 ** 3
|
114
|
+
"kilobytes"
|
111
115
|
else
|
112
|
-
|
116
|
+
"bytes"
|
113
117
|
end
|
114
118
|
end
|
115
119
|
|
@@ -126,7 +130,7 @@ class Filesystems < Component
|
|
126
130
|
filesystem_name: filesystem_name,
|
127
131
|
size: size.to_i * 1024,
|
128
132
|
used: used.to_i * 1024,
|
129
|
-
avail: avail.to_i * 1024
|
133
|
+
avail: avail.to_i * 1024,
|
130
134
|
}
|
131
135
|
end
|
132
136
|
end
|
@@ -1,15 +1,19 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
2
4
|
|
3
5
|
class LastLogin < Component
|
4
6
|
def initialize(motd)
|
5
|
-
super(motd,
|
7
|
+
super(motd, "last_login")
|
6
8
|
end
|
7
9
|
|
10
|
+
# @see Component#process
|
8
11
|
def process
|
9
|
-
@users = @config[
|
10
|
-
@results = parse_last_logins
|
12
|
+
@users = @config["users"]
|
13
|
+
@results = parse_last_logins
|
11
14
|
end
|
12
15
|
|
16
|
+
# @see Component#to_s
|
13
17
|
def to_s
|
14
18
|
<<~HEREDOC
|
15
19
|
Last Login:
|
@@ -19,9 +23,17 @@ class LastLogin < Component
|
|
19
23
|
|
20
24
|
private
|
21
25
|
|
26
|
+
# Takes the list of processed results and generates a complete printable
|
27
|
+
# component string.
|
28
|
+
#
|
29
|
+
# @param user [String] the username to generate a string for
|
30
|
+
# @param logins [Array<Hash>] an array of hashes with username keys and hash
|
31
|
+
# values containing the login data (see {#hashify_login})
|
32
|
+
#
|
33
|
+
# @return [String] the printable string for a single user
|
22
34
|
def parse_result(user, logins)
|
23
35
|
logins_part = if logins.empty?
|
24
|
-
|
36
|
+
" no logins found for user."
|
25
37
|
else
|
26
38
|
longest_size = logins.map { |l| l[:location].length }.max
|
27
39
|
logins.map { |l| parse_login(l, longest_size) }.join("\n")
|
@@ -32,11 +44,18 @@ class LastLogin < Component
|
|
32
44
|
HEREDOC
|
33
45
|
end
|
34
46
|
|
47
|
+
# Takes login data and converts it to a heplful printable string.
|
48
|
+
#
|
49
|
+
# @param login [Hash] the login data, see {#hashify_login}
|
50
|
+
# @param longest_size [Integer] the longest string length to help with string
|
51
|
+
# formatting
|
52
|
+
#
|
53
|
+
# @return [String] the formatted string for printing
|
35
54
|
def parse_login(login, longest_size)
|
36
|
-
location = login[:location].ljust(longest_size,
|
37
|
-
start = login[:time_start].strftime(
|
55
|
+
location = login[:location].ljust(longest_size, " ")
|
56
|
+
start = login[:time_start].strftime("%m/%d/%Y %I:%M%p")
|
38
57
|
finish = if login[:time_end].is_a? String # not a date
|
39
|
-
if login[:time_end] ==
|
58
|
+
if login[:time_end] == "still logged in"
|
40
59
|
login[:time_end].green
|
41
60
|
else
|
42
61
|
login[:time_end].yellow
|
@@ -47,8 +66,12 @@ class LastLogin < Component
|
|
47
66
|
" from #{location} at #{start} (#{finish})"
|
48
67
|
end
|
49
68
|
|
50
|
-
|
51
|
-
|
69
|
+
# Takes a list of configured usernames and grabs login data from the system.
|
70
|
+
#
|
71
|
+
# @return [Hash{Symbol => Hash}] a hash with username keys and hash values
|
72
|
+
# containing the login data (see {#hashify_login})
|
73
|
+
def parse_last_logins
|
74
|
+
@users.map do |(username, num_logins)|
|
52
75
|
cmd_result = `last --time-format=iso #{username}`
|
53
76
|
logins = cmd_result.lines
|
54
77
|
.select { |entry| entry.start_with?(username) }
|
@@ -58,6 +81,13 @@ class LastLogin < Component
|
|
58
81
|
end.to_h
|
59
82
|
end
|
60
83
|
|
84
|
+
# A hash representation of a single login.
|
85
|
+
#
|
86
|
+
# @param login [String] the raw string result from the call to the system
|
87
|
+
# containing various bits of login information
|
88
|
+
# @param username [String] the username of the logged user
|
89
|
+
#
|
90
|
+
# @return [Hash] the parsed login entry data, with symbol keys
|
61
91
|
def hashify_login(login, username)
|
62
92
|
re = login.chomp.split(/(?:\s{2,})|(?<=\d)(?:\s-\s)/)
|
63
93
|
date = re[4].scan(/\d{4}-\d{2}-[\dT:]+-\d{4}/)
|
@@ -65,10 +95,10 @@ class LastLogin < Component
|
|
65
95
|
time_end = date.any? ? Time.parse(re[4]) : re[4]
|
66
96
|
|
67
97
|
{
|
68
|
-
username: username,
|
69
|
-
location: re[2],
|
98
|
+
username: username, # string username
|
99
|
+
location: re[2], # string login location, an IP address
|
70
100
|
time_start: Time.parse(re[3]),
|
71
|
-
time_end: time_end
|
101
|
+
time_end: time_end, # Time or string, could be "still logged in"
|
72
102
|
}
|
73
103
|
end
|
74
104
|
end
|
@@ -1,15 +1,20 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
2
4
|
|
3
5
|
class ServiceStatus < Component
|
4
6
|
def initialize(motd)
|
5
|
-
super(motd,
|
7
|
+
super(motd, "service_status")
|
6
8
|
end
|
7
9
|
|
10
|
+
# @see Component#process
|
8
11
|
def process
|
9
|
-
@services = @config[
|
12
|
+
@services = @config["services"]
|
10
13
|
@results = parse_services(@services)
|
11
14
|
end
|
12
15
|
|
16
|
+
# Gets a printable string to be printed in the MOTD. If there are no services
|
17
|
+
# found in the result, it prints a warning message.
|
13
18
|
def to_s
|
14
19
|
return "Services:\n No matching services found." unless @results.any?
|
15
20
|
|
@@ -17,29 +22,49 @@ class ServiceStatus < Component
|
|
17
22
|
result = <<~HEREDOC
|
18
23
|
Services:
|
19
24
|
#{@results.map do |(name, status)|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
spaces = (" " * (longest_name_size - name.to_s.length + 1))
|
26
|
+
status_part = status.to_s.colorize(service_colors[status.to_sym])
|
27
|
+
" #{name}#{spaces}#{status_part}"
|
28
|
+
end.join("\n")}
|
24
29
|
HEREDOC
|
25
30
|
|
26
|
-
result.gsub(/\s$/,
|
31
|
+
result.gsub(/\s$/, "")
|
27
32
|
end
|
28
33
|
|
29
34
|
private
|
30
35
|
|
36
|
+
# Runs a `systemd` command to determine the state of a service. If the state
|
37
|
+
# of the service was unable to be determined, an error will be added to the
|
38
|
+
# component.
|
39
|
+
#
|
40
|
+
# @param service [String] the name of the systemd service
|
41
|
+
#
|
42
|
+
# @return [String] the state of the systemd service
|
31
43
|
def parse_service(service)
|
32
|
-
cmd_result = `
|
44
|
+
cmd_result = `systemd is-active #{service[0]}`.strip
|
33
45
|
if cmd_result.empty?
|
34
|
-
@errors << ComponentError.new(self,
|
46
|
+
@errors << ComponentError.new(self, "systemctl output was blank.")
|
35
47
|
end
|
36
48
|
cmd_result
|
37
49
|
end
|
38
50
|
|
51
|
+
# Takes a list of services from a configuration file, and turns them into a
|
52
|
+
# hash with the service states as values.
|
53
|
+
#
|
54
|
+
# @param services [Array] a two-element array where the first element is the
|
55
|
+
# name of the systemd service, and the second is the pretty name that
|
56
|
+
# represents it.
|
57
|
+
#
|
58
|
+
# @return [Hash]
|
59
|
+
# * `key`: The symbolized name of the systemd service
|
60
|
+
# * `value`: The symbolized service state
|
39
61
|
def parse_services(services)
|
40
62
|
services.map { |s| [s[1].to_sym, parse_service(s).to_sym] }.to_h
|
41
63
|
end
|
42
64
|
|
65
|
+
# A hash of mappings between a service state and a color which represents it.
|
66
|
+
# The hash has a default value of red in order to handle unexpected service
|
67
|
+
# status strings returned by `systemctl`.
|
43
68
|
def service_colors
|
44
69
|
colors = Hash.new(:red)
|
45
70
|
colors[:active] = :green
|
@@ -1,51 +1,77 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
2
4
|
|
3
5
|
class SSLCertificates < Component
|
4
6
|
def initialize(motd)
|
5
|
-
super(motd,
|
7
|
+
super(motd, "ssl_certificates")
|
6
8
|
end
|
7
9
|
|
10
|
+
# @see Component#process
|
8
11
|
def process
|
9
|
-
@certs = @config[
|
12
|
+
@certs = @config["certs"]
|
10
13
|
@results = cert_dates(@certs)
|
11
14
|
end
|
12
15
|
|
16
|
+
# Prints the list of SSL certificates with their statuses. If a certificate
|
17
|
+
# is not found at the configured location, a message will be printed which
|
18
|
+
# explains this.
|
13
19
|
def to_s
|
14
20
|
<<~HEREDOC
|
15
21
|
SSL Certificates:
|
16
22
|
#{sorted_results.map do |cert|
|
17
|
-
|
23
|
+
return " #{cert}" if cert.is_a? String # print the not found message
|
18
24
|
|
19
|
-
|
20
|
-
|
25
|
+
parse_cert(cert)
|
26
|
+
end.join("\n")}
|
21
27
|
HEREDOC
|
22
28
|
end
|
23
29
|
|
24
30
|
private
|
25
31
|
|
32
|
+
# Takes an entry from `@results` and formats it in a way that is conducive
|
33
|
+
# to being printed in the context of the MOTD.
|
34
|
+
#
|
35
|
+
# @param cert [Array] a two-element array in the same format as the return
|
36
|
+
# value of {#parse_result}
|
26
37
|
def parse_cert(cert)
|
27
|
-
name_portion = cert[0].ljust(longest_cert_name_length + 6,
|
38
|
+
name_portion = cert[0].ljust(longest_cert_name_length + 6, " ")
|
28
39
|
status_sym = cert_status(cert[1])
|
29
40
|
status = cert_status_strings[status_sym].to_s
|
30
41
|
colorized_status = status.colorize(cert_status_colors[status_sym])
|
31
|
-
date_portion = cert[1].strftime(
|
42
|
+
date_portion = cert[1].strftime("%e %b %Y %H:%M:%S%p")
|
32
43
|
" #{name_portion} #{colorized_status} #{date_portion}"
|
33
44
|
end
|
34
45
|
|
46
|
+
# Determines the length of the longest SSL certificate name for use in
|
47
|
+
# formatting the output of the component.
|
48
|
+
#
|
49
|
+
# @return [Integer] the length of the longest certificate name
|
35
50
|
def longest_cert_name_length
|
36
51
|
@results.map { |r| r[0].length }.max
|
37
52
|
end
|
38
53
|
|
54
|
+
# Takes the results array and sorts it according to the configured sort
|
55
|
+
# method. If the option is not set or is set improperly, it will default to
|
56
|
+
# alphabetical.
|
39
57
|
def sorted_results
|
40
|
-
if @config[
|
58
|
+
if @config["sort_method"] == "alphabetical"
|
41
59
|
@results.sort_by { |c| c[0] }
|
42
|
-
elsif @config[
|
60
|
+
elsif @config["sort_method"] == "expiration"
|
43
61
|
@results.sort_by { |c| c[1] }
|
44
62
|
else # default to alphabetical
|
45
63
|
@results.sort_by { |c| c[0] }
|
46
64
|
end
|
47
65
|
end
|
48
66
|
|
67
|
+
# Takes a list of certificates and compiles a list of results for each
|
68
|
+
# certificate. If a certificate was not found, a notice will be returned
|
69
|
+
# instead.
|
70
|
+
#
|
71
|
+
# @return [Array] An array of parsed results. If there was an error, the
|
72
|
+
# element will be just a string. If it was successful, the element will be
|
73
|
+
# another two-element array in the same format as the return value of
|
74
|
+
# {#parse_result}.
|
49
75
|
def cert_dates(certs)
|
50
76
|
certs.map do |name, path|
|
51
77
|
if File.exist?(path)
|
@@ -56,26 +82,41 @@ class SSLCertificates < Component
|
|
56
82
|
end.compact # remove nil entries, will have nil if error ocurred
|
57
83
|
end
|
58
84
|
|
85
|
+
# Uses `openssl` to obtain and parse and expiration date for the certificate.
|
86
|
+
#
|
87
|
+
# @param name [String] the name of the SSL certificate
|
88
|
+
# @param path [String] the file path to the SSL certificate
|
89
|
+
#
|
90
|
+
# @return [Array] A pair where the first element is the configured name of
|
91
|
+
# the SSL certificate, and the second element is the expiration date of
|
92
|
+
# the certificate.
|
59
93
|
def parse_result(name, path)
|
60
94
|
cmd_result = `openssl x509 -in #{path} -dates`
|
61
95
|
# match indices: 1 - month, 2 - day, 3 - time, 4 - year, 5 - zone
|
62
|
-
exp = /notAfter=([A-Za-z]+) (\d+) ([\d:]+) (\d{4}) ([A-Za-z]+)\n/
|
96
|
+
exp = /notAfter=([A-Za-z]+) +(\d+) +([\d:]+) +(\d{4}) +([A-Za-z]+)\n/
|
63
97
|
parsed = cmd_result.match(exp)
|
64
98
|
|
65
99
|
if parsed.nil?
|
66
|
-
@errors << ComponentError.new(self,
|
67
|
-
|
100
|
+
@errors << ComponentError.new(self, "Unable to find certificate " \
|
101
|
+
"expiration date")
|
68
102
|
nil
|
69
103
|
else
|
70
|
-
expiry_date = Time.parse([1, 2, 4, 3, 5].map { |n| parsed[n] }.join(
|
104
|
+
expiry_date = Time.parse([1, 2, 4, 3, 5].map { |n| parsed[n] }.join(" "))
|
71
105
|
[name, expiry_date]
|
72
106
|
end
|
73
107
|
rescue ArgumentError
|
74
|
-
@errors << ComponentError.new(self,
|
75
|
-
|
108
|
+
@errors << ComponentError.new(self, "Found expiration date, but unable " \
|
109
|
+
"to parse as date")
|
76
110
|
[name, Time.now]
|
77
111
|
end
|
78
112
|
|
113
|
+
# Maps an expiration date to a symbol representing the expiration status of
|
114
|
+
# an SSL certificate.
|
115
|
+
#
|
116
|
+
# @param expiry_date [Time] the time at which the certificate expires
|
117
|
+
#
|
118
|
+
# @return [Symbol] A symbol representing the expiration status of the
|
119
|
+
# certificate. Valid values are `:expiring`, `:expired`, and `:valid`.
|
79
120
|
def cert_status(expiry_date)
|
80
121
|
if (Time.now...Time.now + 30).cover? expiry_date # ... range excludes last
|
81
122
|
:expiring
|
@@ -86,19 +127,22 @@ class SSLCertificates < Component
|
|
86
127
|
end
|
87
128
|
end
|
88
129
|
|
130
|
+
# Maps a certificate expiration status to a color that represents it.
|
89
131
|
def cert_status_colors
|
90
132
|
{
|
91
133
|
valid: :green,
|
92
134
|
expiring: :yellow,
|
93
|
-
expired: :red
|
135
|
+
expired: :red,
|
94
136
|
}
|
95
137
|
end
|
96
138
|
|
139
|
+
# Maps a certificate expiration status to a string which can be prefixed to
|
140
|
+
# the expiration date, to aid in explaining when the certificate expires.
|
97
141
|
def cert_status_strings
|
98
142
|
{
|
99
|
-
valid:
|
100
|
-
expiring:
|
101
|
-
expired:
|
143
|
+
valid: "valid until",
|
144
|
+
expiring: "expiring at",
|
145
|
+
expired: "expired at",
|
102
146
|
}
|
103
147
|
end
|
104
148
|
end
|
@@ -1,12 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sysinfo"
|
2
4
|
|
3
5
|
class Uptime < Component
|
4
6
|
attr_reader :days, :hours, :minutes
|
5
7
|
|
6
8
|
def initialize(motd)
|
7
|
-
super(motd,
|
9
|
+
super(motd, "uptime")
|
8
10
|
end
|
9
11
|
|
12
|
+
# Calculates the number of days, hours, and minutes based on the current
|
13
|
+
# uptime value.
|
14
|
+
#
|
15
|
+
# @see Component#process
|
10
16
|
def process
|
11
17
|
uptime = SysInfo.new.uptime
|
12
18
|
|
@@ -15,16 +21,27 @@ class Uptime < Component
|
|
15
21
|
@minutes = ((uptime - @days * 24 - hours) * 60).floor
|
16
22
|
end
|
17
23
|
|
24
|
+
# Gets a printable uptime string with a prefix. The prefix can be configured,
|
25
|
+
# and defaults to "up".
|
18
26
|
def to_s
|
19
|
-
"#{@config[
|
27
|
+
"#{@config["prefix"] || "up"} #{format_uptime}"
|
20
28
|
end
|
21
29
|
|
22
30
|
private
|
23
31
|
|
32
|
+
# Formats the uptime values in such a way that it is easier to read. If any
|
33
|
+
# of the measurements are zero, that part is omitted. Words are properly
|
34
|
+
# pluralized.
|
35
|
+
#
|
36
|
+
# Examples:
|
37
|
+
#
|
38
|
+
# `3d 20h 55m` becomes `3 days, 20 hours, 55 minutes`
|
39
|
+
#
|
40
|
+
# `3d 0h 55m` becomes `3 days, 55 minutes`
|
24
41
|
def format_uptime
|
25
42
|
[@days, @hours, @minutes].zip(%w[day hour minute])
|
26
43
|
.reject { |n, _word| n.zero? }
|
27
|
-
.map { |n, word| "#{n} #{word}#{
|
28
|
-
.join(
|
44
|
+
.map { |n, word| "#{n} #{word}#{"s" if n != 1}" }
|
45
|
+
.join(", ")
|
29
46
|
end
|
30
47
|
end
|
data/lib/panda_motd/config.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
3
5
|
|
4
6
|
class Config
|
5
7
|
attr_reader :file_path
|
6
8
|
|
9
|
+
# @param file_path [String] The file path to look for the configuration file.
|
10
|
+
# If not provided, the default file path will be used.
|
7
11
|
def initialize(file_path = nil)
|
8
|
-
@file_path = file_path || File.join(Dir.home,
|
12
|
+
@file_path = file_path || File.join(Dir.home, ".config", "panda-motd.yaml")
|
9
13
|
unless File.exist?(@file_path)
|
10
14
|
create_config(@file_path)
|
11
15
|
puts "panda-motd created a default config file at: #{@file_path}"
|
@@ -13,17 +17,23 @@ class Config
|
|
13
17
|
load_config(@file_path)
|
14
18
|
end
|
15
19
|
|
20
|
+
# A list of enabled components' class constants.
|
16
21
|
def components_enabled
|
17
22
|
# iterate config hash and grab names of enabled components
|
18
|
-
enabled = @config[
|
23
|
+
enabled = @config["components"].map { |c, s| c if s["enabled"] }.compact
|
19
24
|
# get the class constant
|
20
25
|
enabled.map { |e| Config.component_classes[e.to_sym] }
|
21
26
|
end
|
22
27
|
|
28
|
+
# Gets the configuration for a component.
|
29
|
+
#
|
30
|
+
# @param component_name [String] the name of the component to fetch the
|
31
|
+
# configuration for
|
23
32
|
def component_config(component_name)
|
24
|
-
@config[
|
33
|
+
@config["components"][component_name.to_s]
|
25
34
|
end
|
26
35
|
|
36
|
+
# The mapping of component string names to class constants.
|
27
37
|
def self.component_classes
|
28
38
|
{
|
29
39
|
ascii_text_art: ASCIITextArt,
|
@@ -32,21 +42,28 @@ class Config
|
|
32
42
|
ssl_certificates: SSLCertificates,
|
33
43
|
filesystems: Filesystems,
|
34
44
|
last_login: LastLogin,
|
35
|
-
fail_2_ban: Fail2Ban
|
45
|
+
fail_2_ban: Fail2Ban,
|
36
46
|
}
|
37
47
|
end
|
38
48
|
|
39
49
|
private
|
40
50
|
|
51
|
+
# Creates a configuration file at a given file path, from the default
|
52
|
+
# configuration file.
|
53
|
+
#
|
54
|
+
# @param file_path [String] the file path at which to create the config
|
41
55
|
def create_config(file_path)
|
42
56
|
default_config_path = File.join(
|
43
|
-
File.dirname(__dir__),
|
57
|
+
File.dirname(__dir__), "panda_motd", "default_config.yaml"
|
44
58
|
)
|
45
59
|
FileUtils.cp(default_config_path, file_path)
|
46
60
|
end
|
47
61
|
|
62
|
+
# Loads a configuration file.
|
63
|
+
#
|
64
|
+
# @param file_path [String] the file path of the config to load
|
48
65
|
def load_config(file_path)
|
49
66
|
@config = YAML.safe_load(File.read(file_path))
|
50
|
-
@config[
|
67
|
+
@config["components"] = [] if @config["components"].nil?
|
51
68
|
end
|
52
69
|
end
|
data/lib/panda_motd/motd.rb
CHANGED
@@ -1,14 +1,27 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sysinfo"
|
2
4
|
|
3
5
|
class MOTD
|
4
6
|
attr_reader :config, :components
|
5
7
|
|
8
|
+
# Creates an MOTD by parsing the provided config file, and processing each
|
9
|
+
# component.
|
10
|
+
#
|
11
|
+
# @param config_path [String] The path to the configuration file. If not
|
12
|
+
# provided, the default config path will be used.
|
13
|
+
# @param process [Boolean] whether or not to actually process and evaluate
|
14
|
+
# the printable results of each component
|
6
15
|
def initialize(config_path = nil, process = true)
|
7
|
-
@config =
|
16
|
+
@config = Config.new(config_path)
|
8
17
|
@components = @config.components_enabled.map { |ce| ce.new(self) }
|
9
18
|
@components.each(&:process) if process
|
10
19
|
end
|
11
20
|
|
21
|
+
# Takes each component on the MOTD and joins them together in a printable
|
22
|
+
# format. It inserts two newlines in between each component, ensuring that
|
23
|
+
# there is one empty line between each. If a component has any errors, the
|
24
|
+
# error will be printed in a clean way.
|
12
25
|
def to_s
|
13
26
|
@components.map do |c|
|
14
27
|
if c.errors.any?
|
data/lib/panda_motd/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: panda-motd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taylor Thurlow
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: artii
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
153
167
|
- !ruby/object:Gem::Dependency
|
154
168
|
name: rspec
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -192,6 +206,20 @@ dependencies:
|
|
192
206
|
- - ">="
|
193
207
|
- !ruby/object:Gem::Version
|
194
208
|
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: rufo
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
195
223
|
- !ruby/object:Gem::Dependency
|
196
224
|
name: simplecov
|
197
225
|
requirement: !ruby/object:Gem::Requirement
|
@@ -206,8 +234,22 @@ dependencies:
|
|
206
234
|
- - ">="
|
207
235
|
- !ruby/object:Gem::Version
|
208
236
|
version: '0'
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
name: solargraph
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - ">="
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
type: :development
|
245
|
+
prerelease: false
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
247
|
+
requirements:
|
248
|
+
- - ">="
|
249
|
+
- !ruby/object:Gem::Version
|
250
|
+
version: '0'
|
209
251
|
description: Enhance your MOTD with useful at-a-glance information.
|
210
|
-
email:
|
252
|
+
email: taylorthurlow@me.com
|
211
253
|
executables:
|
212
254
|
- panda-motd
|
213
255
|
extensions: []
|
@@ -232,7 +274,7 @@ homepage: https://github.com/taylorthurlow/panda-motd
|
|
232
274
|
licenses:
|
233
275
|
- MIT
|
234
276
|
metadata: {}
|
235
|
-
post_install_message:
|
277
|
+
post_install_message:
|
236
278
|
rdoc_options: []
|
237
279
|
require_paths:
|
238
280
|
- lib
|
@@ -240,15 +282,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
240
282
|
requirements:
|
241
283
|
- - ">="
|
242
284
|
- !ruby/object:Gem::Version
|
243
|
-
version: '2.
|
285
|
+
version: '2.4'
|
244
286
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
245
287
|
requirements:
|
246
288
|
- - ">="
|
247
289
|
- !ruby/object:Gem::Version
|
248
290
|
version: '0'
|
249
291
|
requirements: []
|
250
|
-
|
251
|
-
|
292
|
+
rubyforge_project:
|
293
|
+
rubygems_version: 2.6.14.4
|
294
|
+
signing_key:
|
252
295
|
specification_version: 4
|
253
296
|
summary: Make your MOTD prettier, and more useful.
|
254
297
|
test_files: []
|