panda-motd 0.0.8 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/panda_motd.rb +5 -1
- data/lib/panda_motd/component.rb +26 -0
- data/lib/panda_motd/components/ascii_text_art.rb +8 -16
- data/lib/panda_motd/components/fail_2_ban.rb +44 -0
- data/lib/panda_motd/components/filesystems.rb +86 -62
- data/lib/panda_motd/components/last_login.rb +52 -48
- data/lib/panda_motd/components/service_status.rb +13 -14
- data/lib/panda_motd/components/ssl_certificates.rb +53 -31
- data/lib/panda_motd/components/uptime.rb +3 -7
- data/lib/panda_motd/config.rb +16 -11
- data/lib/panda_motd/default_config.yaml +21 -1
- data/lib/panda_motd/motd.rb +3 -3
- data/lib/panda_motd/version.rb +1 -1
- metadata +62 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7036e8cca753cf96292eb0c4dd7bb29524852e486265b5de0403089af3f7897d
|
4
|
+
data.tar.gz: c9237796bc85552104a451801e202a0bd57e139f4c2bafc2cea9d2275ff92291
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8336450885b5e4f51d1afe19c75a18f3c3eca1928cc1ce9967287933960f0e695c2f6d2908ad3820e0cfcd705ade94388c9aa9657a9a894a0a1ee8d59090df03
|
7
|
+
data.tar.gz: f5b94c6be1aa9859735f0aa3eddeba09283c76664930127724ab628f29a82e917fbb1f63494ffa626514e82fd4e33891bbe607c7fa6222d506ec259d384a0e85
|
data/lib/panda_motd.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Component
|
2
|
+
attr_reader :name, :errors, :results, :config
|
3
|
+
|
4
|
+
def initialize(motd, name)
|
5
|
+
@name = name
|
6
|
+
@motd = motd
|
7
|
+
@config = motd.config.component_config(@name)
|
8
|
+
@errors = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def process
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def lines_before
|
20
|
+
@motd.config.component_config(@name)['lines_before'] || 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def lines_after
|
24
|
+
@motd.config.component_config(@name)['lines_after'] || 1
|
25
|
+
end
|
26
|
+
end
|
@@ -1,29 +1,21 @@
|
|
1
1
|
require 'artii'
|
2
2
|
require 'colorize'
|
3
3
|
|
4
|
-
class ASCIITextArt
|
5
|
-
attr_reader :name, :errors, :results
|
6
|
-
|
4
|
+
class ASCIITextArt < Component
|
7
5
|
def initialize(motd)
|
8
|
-
|
9
|
-
@motd = motd
|
10
|
-
@config = motd.config.component_config(@name)
|
11
|
-
@errors = []
|
6
|
+
super(motd, 'ascii_text_art')
|
12
7
|
end
|
13
8
|
|
14
9
|
def process
|
15
10
|
@text = `#{@config['command']}`
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
|
22
|
-
@errors << ComponentError.new(self, 'Invalid font name')
|
23
|
-
end
|
11
|
+
@art = Artii::Base.new font: @config['font']
|
12
|
+
@results = @art.asciify(@text)
|
13
|
+
@results = @results.colorize(@config['color'].to_sym) if @config['color']
|
14
|
+
rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
|
15
|
+
@errors << ComponentError.new(self, 'Invalid font name')
|
24
16
|
end
|
25
17
|
|
26
18
|
def to_s
|
27
|
-
|
19
|
+
@results
|
28
20
|
end
|
29
21
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class Fail2Ban < Component
|
2
|
+
def initialize(motd)
|
3
|
+
super(motd, 'fail_2_ban')
|
4
|
+
end
|
5
|
+
|
6
|
+
def process
|
7
|
+
@results = {
|
8
|
+
jails: {}
|
9
|
+
}
|
10
|
+
|
11
|
+
@config['jails'].each do |jail|
|
12
|
+
status = jail_status(jail)
|
13
|
+
@results[:jails][jail] = {
|
14
|
+
total: status[:total],
|
15
|
+
current: status[:current]
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
result = "Fail2Ban:\n"
|
22
|
+
@results[:jails].each do |name, stats|
|
23
|
+
result += " #{name}:\n"
|
24
|
+
result += " Total bans: #{stats[:total]}\n"
|
25
|
+
result += " Current bans: #{stats[:current]}\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
result.gsub(/\s$/, '')
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def jail_status(jail)
|
34
|
+
cmd_result = `fail2ban-client status #{jail}`
|
35
|
+
if cmd_result =~ /Sorry but the jail '#{jail}' does not exist/
|
36
|
+
@errors << ComponentError.new(self, "Invalid jail name '#{jail}'.")
|
37
|
+
else
|
38
|
+
total = cmd_result.match(/Total banned:\s+([0-9]+)/)[1].to_i
|
39
|
+
current = cmd_result.match(/Currently banned:\s+([0-9]+)/)[1].to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
{ total: total, current: current }
|
43
|
+
end
|
44
|
+
end
|
@@ -1,15 +1,9 @@
|
|
1
1
|
require 'ruby-units'
|
2
2
|
require 'colorize'
|
3
3
|
|
4
|
-
class Filesystems
|
5
|
-
attr_reader :name, :errors
|
6
|
-
attr_reader :results
|
7
|
-
|
4
|
+
class Filesystems < Component
|
8
5
|
def initialize(motd)
|
9
|
-
|
10
|
-
@motd = motd
|
11
|
-
@config = motd.config.component_config(@name)
|
12
|
-
@errors = []
|
6
|
+
super(motd, 'filesystems')
|
13
7
|
end
|
14
8
|
|
15
9
|
def process
|
@@ -17,69 +11,56 @@ class Filesystems
|
|
17
11
|
end
|
18
12
|
|
19
13
|
def to_s
|
20
|
-
name_col_size = @results.select { |r| r.is_a? Hash }
|
21
|
-
|
14
|
+
name_col_size = @results.select { |r| r.is_a? Hash }
|
15
|
+
.map { |r| r[:pretty_name].length }
|
16
|
+
.max || 0
|
22
17
|
|
23
|
-
|
18
|
+
size_w_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
|
24
19
|
|
25
|
-
result = 'Filesystems'.ljust(
|
20
|
+
result = 'Filesystems'.ljust(size_w_padding, ' ')
|
26
21
|
result += "Size Used Free Use%\n"
|
27
22
|
|
28
23
|
@results.each do |filesystem|
|
29
|
-
|
30
|
-
if filesystem.is_a? String
|
31
|
-
result += " #{filesystem}\n"
|
32
|
-
next
|
33
|
-
end
|
34
|
-
|
35
|
-
result += " #{filesystem[:pretty_name]}".ljust(name_col_size_with_padding, ' ')
|
36
|
-
|
37
|
-
[:size, :used, :avail].each do |metric|
|
38
|
-
units = if filesystem[metric] > 10**12
|
39
|
-
'terabytes'
|
40
|
-
elsif filesystem[metric] > 10**9
|
41
|
-
'gigabytes'
|
42
|
-
elsif filesystem[metric] > 10**6
|
43
|
-
'megabytes'
|
44
|
-
elsif filesystem[metric] > 10**3
|
45
|
-
'kilobytes'
|
46
|
-
else
|
47
|
-
'bytes'
|
48
|
-
end
|
49
|
-
|
50
|
-
value = Unit.new("#{filesystem[metric]} bytes").convert_to(units)
|
51
|
-
|
52
|
-
# we have 4 characters of space to include the number, a potential decimal point, and
|
53
|
-
# the unit character at the end. if the whole number component is 3+ digits long then
|
54
|
-
# we omit the decimal point and just display the whole number component. if the whole
|
55
|
-
# number component is 2 digits long, we can't afford to use a decimal point, so we still
|
56
|
-
# only display the whole number component. if the whole number component is 1 digit long,
|
57
|
-
# we use the single whole number component digit, a decimal point, a single fractional
|
58
|
-
# digit, and the unit character.
|
59
|
-
whole_number_length = value.scalar.floor.to_s.length
|
60
|
-
round_amount = whole_number_length > 1 ? 0 : 1
|
61
|
-
formatted_value = value.scalar.round(round_amount).to_s + units[0].upcase
|
62
|
-
result += formatted_value.rjust(4, ' ') + ' '
|
63
|
-
end
|
64
|
-
|
65
|
-
percentage_used = ((filesystem[:used].to_f / filesystem[:size].to_f) * 100).round
|
66
|
-
result += (percentage_used.to_s.rjust(3, ' ') + '%').send(percentage_color(percentage_used))
|
67
|
-
|
68
|
-
result += "\n"
|
69
|
-
|
70
|
-
total_ticks = name_col_size_with_padding + 18
|
71
|
-
used_ticks = (total_ticks * (percentage_used.to_f / 100)).round
|
72
|
-
|
73
|
-
result += " [#{('=' * used_ticks).send(percentage_color(percentage_used))}#{('=' * (total_ticks - used_ticks)).light_black}]"
|
74
|
-
result += "\n" unless filesystem == @results.last
|
24
|
+
result += format_filesystem(filesystem, size_w_padding)
|
75
25
|
end
|
76
26
|
|
77
|
-
|
27
|
+
result
|
78
28
|
end
|
79
29
|
|
80
30
|
private
|
81
31
|
|
82
|
-
def
|
32
|
+
def format_filesystem(filesystem, size)
|
33
|
+
return " #{filesystem}\n" if filesystem.is_a? String # handle fs not found
|
34
|
+
|
35
|
+
# filesystem name
|
36
|
+
result = ''
|
37
|
+
result += " #{filesystem[:pretty_name]}".ljust(size, ' ')
|
38
|
+
|
39
|
+
# statistics (size, used, free, use%)
|
40
|
+
[:size, :used, :avail].each do |metric|
|
41
|
+
result += format_metric(filesystem, metric)
|
42
|
+
end
|
43
|
+
percent_used = calc_percent_used(filesystem)
|
44
|
+
result += format_percent_used(percent_used)
|
45
|
+
result += "\n"
|
46
|
+
|
47
|
+
# visual bar representation of use%
|
48
|
+
result += generate_usage_bar(filesystem, size, percent_used)
|
49
|
+
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
def generate_usage_bar(filesystem, size, percent_used)
|
54
|
+
result = ''
|
55
|
+
total_ticks = size + 18
|
56
|
+
used_ticks = (total_ticks * (percent_used.to_f / 100)).round
|
57
|
+
result += " [#{('=' * used_ticks).send(pct_color(percent_used))}"\
|
58
|
+
"#{('=' * (total_ticks - used_ticks)).light_black}]"
|
59
|
+
result += "\n" unless filesystem == @results.last
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
def pct_color(percentage)
|
83
64
|
case percentage
|
84
65
|
when 0..75 then :green
|
85
66
|
when 76..95 then :yellow
|
@@ -88,9 +69,52 @@ class Filesystems
|
|
88
69
|
end
|
89
70
|
end
|
90
71
|
|
72
|
+
def calc_percent_used(filesystem)
|
73
|
+
((filesystem[:used].to_f / filesystem[:size].to_f) * 100).round
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_percent_used(percent_used)
|
77
|
+
(percent_used.to_s.rjust(3, ' ') + '%').send(pct_color(percent_used))
|
78
|
+
end
|
79
|
+
|
80
|
+
def calc_metric(value)
|
81
|
+
Unit.new("#{value} bytes").convert_to(calc_units(value))
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_metric(filesystem, metric)
|
85
|
+
# we have 4 characters of space to include the number, a potential
|
86
|
+
# decimal point, and the unit character at the end. if the whole number
|
87
|
+
# component is 3+ digits long then we omit the decimal point and just
|
88
|
+
# display the whole number component. if the whole number component is
|
89
|
+
# 2 digits long, we can't afford to use a decimal point, so we still
|
90
|
+
# only display the whole number component. if the whole number
|
91
|
+
# component is 1 digit long, we use the single whole number component
|
92
|
+
# digit, a decimal point, a single fractional digit, and the unit
|
93
|
+
# character.
|
94
|
+
|
95
|
+
value = calc_metric(filesystem[metric])
|
96
|
+
whole_number_length = value.scalar.floor.to_s.length
|
97
|
+
round_amount = whole_number_length > 1 ? 0 : 1
|
98
|
+
formatted = value.scalar.round(round_amount).to_s + value.units[0].upcase
|
99
|
+
formatted.rjust(4, ' ') + ' '
|
100
|
+
end
|
101
|
+
|
102
|
+
def calc_units(value)
|
103
|
+
if value > 10**12
|
104
|
+
'terabytes'
|
105
|
+
elsif value > 10**9
|
106
|
+
'gigabytes'
|
107
|
+
elsif value > 10**6
|
108
|
+
'megabytes'
|
109
|
+
elsif value > 10**3
|
110
|
+
'kilobytes'
|
111
|
+
else
|
112
|
+
'bytes'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
91
116
|
def parse_filesystem_usage(filesystems)
|
92
|
-
entries = `BLOCKSIZE=1024 df --output=source,size,used,avail`.lines
|
93
|
-
.drop(1)
|
117
|
+
entries = `BLOCKSIZE=1024 df --output=source,size,used,avail`.lines.drop(1)
|
94
118
|
|
95
119
|
filesystems.map do |filesystem, pretty_name|
|
96
120
|
matching_entry = entries.map(&:split).find { |e| e.first == filesystem }
|
@@ -1,13 +1,8 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
|
-
class LastLogin
|
4
|
-
attr_reader :name, :errors, :results
|
5
|
-
|
3
|
+
class LastLogin < Component
|
6
4
|
def initialize(motd)
|
7
|
-
|
8
|
-
@motd = motd
|
9
|
-
@config = motd.config.component_config(@name)
|
10
|
-
@errors = []
|
5
|
+
super(motd, 'last_login')
|
11
6
|
end
|
12
7
|
|
13
8
|
def process
|
@@ -16,55 +11,64 @@ class LastLogin
|
|
16
11
|
end
|
17
12
|
|
18
13
|
def to_s
|
19
|
-
<<~
|
14
|
+
<<~HEREDOC
|
20
15
|
Last Login:
|
21
|
-
#{@results.map
|
22
|
-
|
23
|
-
if logins.empty?
|
24
|
-
' no logins found for user.'
|
25
|
-
else
|
26
|
-
longest_location_size = logins.map { |l| l[:location].length }.max
|
27
|
-
logins.map do |login|
|
28
|
-
location_part = login[:location].ljust(longest_location_size, ' ')
|
29
|
-
start_part = login[:time_start].strftime('%m/%d/%Y %I:%M%p')
|
30
|
-
end_part = if login[:time_end].is_a? String # still logged in text
|
31
|
-
login[:time_end].green
|
32
|
-
else
|
33
|
-
"#{((login[:time_end] - login[:time_start]) * 24 * 60).to_i} minutes"
|
34
|
-
end
|
35
|
-
" from #{location_part} at #{start_part} (#{end_part})"
|
36
|
-
end.join("\n")
|
37
|
-
end
|
38
|
-
<<~USER
|
39
|
-
#{user}:
|
40
|
-
#{logins_part}
|
41
|
-
USER
|
42
|
-
end.join("\n")}
|
43
|
-
LAST
|
16
|
+
#{@results.map { |u, l| parse_result(u, l) }.join("\n")}
|
17
|
+
HEREDOC
|
44
18
|
end
|
45
19
|
|
46
20
|
private
|
47
21
|
|
22
|
+
def parse_result(user, logins)
|
23
|
+
logins_part = if logins.empty?
|
24
|
+
' no logins found for user.'
|
25
|
+
else
|
26
|
+
longest_size = logins.map { |l| l[:location].length }.max
|
27
|
+
logins.map { |l| parse_login(l, longest_size) }.join("\n")
|
28
|
+
end
|
29
|
+
<<~HEREDOC
|
30
|
+
#{user}:
|
31
|
+
#{logins_part}
|
32
|
+
HEREDOC
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_login(login, longest_size)
|
36
|
+
location = login[:location].ljust(longest_size, ' ')
|
37
|
+
start = login[:time_start].strftime('%m/%d/%Y %I:%M%p')
|
38
|
+
finish = if login[:time_end].is_a? String # not a date
|
39
|
+
if login[:time_end] == 'still logged in'
|
40
|
+
login[:time_end].green
|
41
|
+
else
|
42
|
+
login[:time_end].yellow
|
43
|
+
end
|
44
|
+
else
|
45
|
+
"#{((login[:time_end] - login[:time_start]) / 60).to_i} minutes"
|
46
|
+
end
|
47
|
+
" from #{location} at #{start} (#{finish})"
|
48
|
+
end
|
49
|
+
|
48
50
|
def parse_last_logins(users)
|
49
51
|
users.map do |(username, num_logins)|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
.take(num_logins)
|
55
|
-
.map do |entry|
|
56
|
-
data = entry.chomp.split(/(?:\s{2,})|(?:\s-\s)/)
|
57
|
-
time_end = data[4] == 'still logged in' ? data[4] : DateTime.parse(data[4])
|
52
|
+
cmd_result = `last --time-format=iso #{username}`
|
53
|
+
logins = cmd_result.lines
|
54
|
+
.select { |entry| entry.start_with?(username) }
|
55
|
+
.take(num_logins)
|
58
56
|
|
59
|
-
|
60
|
-
username: username,
|
61
|
-
location: data[2],
|
62
|
-
time_start: DateTime.parse(data[3]),
|
63
|
-
time_end: time_end
|
64
|
-
}
|
65
|
-
end
|
66
|
-
|
67
|
-
[username.to_sym, user_logins]
|
57
|
+
[username.to_sym, logins.map! { |l| hashify_login(l, username) }]
|
68
58
|
end.to_h
|
69
59
|
end
|
60
|
+
|
61
|
+
def hashify_login(login, username)
|
62
|
+
re = login.chomp.split(/(?:\s{2,})|(?<=\d)(?:\s-\s)/)
|
63
|
+
date = re[4].scan(/\d{4}-\d{2}-[\dT:]+-\d{4}/)
|
64
|
+
|
65
|
+
time_end = date.any? ? Time.parse(re[4]) : re[4]
|
66
|
+
|
67
|
+
{
|
68
|
+
username: username,
|
69
|
+
location: re[2],
|
70
|
+
time_start: Time.parse(re[3]),
|
71
|
+
time_end: time_end
|
72
|
+
}
|
73
|
+
end
|
70
74
|
end
|
@@ -1,14 +1,8 @@
|
|
1
1
|
require 'colorize'
|
2
2
|
|
3
|
-
class ServiceStatus
|
4
|
-
attr_reader :name, :errors
|
5
|
-
attr_reader :results
|
6
|
-
|
3
|
+
class ServiceStatus < Component
|
7
4
|
def initialize(motd)
|
8
|
-
|
9
|
-
@motd = motd
|
10
|
-
@config = motd.config.component_config(@name)
|
11
|
-
@errors = []
|
5
|
+
super(motd, 'service_status')
|
12
6
|
end
|
13
7
|
|
14
8
|
def process
|
@@ -18,8 +12,9 @@ class ServiceStatus
|
|
18
12
|
|
19
13
|
def to_s
|
20
14
|
return "Services:\n No matching services found." unless @results.any?
|
15
|
+
|
21
16
|
longest_name_size = @results.keys.map { |k| k.to_s.length }.max
|
22
|
-
<<~HEREDOC
|
17
|
+
result = <<~HEREDOC
|
23
18
|
Services:
|
24
19
|
#{@results.map do |(name, status)|
|
25
20
|
name_part = name.to_s.ljust(longest_name_size, ' ') + ':'
|
@@ -27,28 +22,32 @@ class ServiceStatus
|
|
27
22
|
" #{name_part} #{status_part}"
|
28
23
|
end.join("\n")}
|
29
24
|
HEREDOC
|
25
|
+
|
26
|
+
result.gsub(/\s$/, '')
|
30
27
|
end
|
31
28
|
|
32
29
|
private
|
33
30
|
|
34
31
|
def parse_service(service)
|
35
32
|
cmd_result = `systemctl is-active #{service[0]}`.strip
|
36
|
-
|
37
|
-
|
33
|
+
unless valid_responses.include? cmd_result
|
34
|
+
@errors << ComponentError.new(self, 'Unable to parse systemctl output')
|
35
|
+
end
|
36
|
+
cmd_result
|
38
37
|
end
|
39
38
|
|
40
39
|
def parse_services(services)
|
41
|
-
services.map { |
|
40
|
+
services.map { |s| [s[1].to_sym, parse_service(s).to_sym] }.to_h
|
42
41
|
end
|
43
42
|
|
44
43
|
def service_colors
|
45
|
-
|
44
|
+
{
|
46
45
|
active: :green,
|
47
46
|
inactive: :red
|
48
47
|
}
|
49
48
|
end
|
50
49
|
|
51
50
|
def valid_responses
|
52
|
-
|
51
|
+
['active', 'inactive']
|
53
52
|
end
|
54
53
|
end
|
@@ -1,13 +1,8 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
|
-
class SSLCertificates
|
4
|
-
attr_reader :name, :errors, :results
|
5
|
-
|
3
|
+
class SSLCertificates < Component
|
6
4
|
def initialize(motd)
|
7
|
-
|
8
|
-
@motd = motd
|
9
|
-
@config = motd.config.component_config(@name)
|
10
|
-
@errors = []
|
5
|
+
super(motd, 'ssl_certificates')
|
11
6
|
end
|
12
7
|
|
13
8
|
def process
|
@@ -16,48 +11,75 @@ class SSLCertificates
|
|
16
11
|
end
|
17
12
|
|
18
13
|
def to_s
|
19
|
-
longest_name_size = @results.map { |r| r[0].length }.max
|
20
14
|
<<~HEREDOC
|
21
15
|
SSL Certificates:
|
22
|
-
#{
|
16
|
+
#{sorted_results.map do |cert|
|
23
17
|
return " #{cert}" if cert.is_a? String # print the not found message
|
24
18
|
|
25
|
-
|
26
|
-
status = cert_status(cert[1])
|
27
|
-
status = cert_status_strings[status].to_s.colorize(cert_status_colors[status])
|
28
|
-
date_portion = cert[1].strftime('%e %b %Y %H:%M:%S%p')
|
29
|
-
" #{name_portion} #{status} #{date_portion}"
|
19
|
+
parse_cert(cert)
|
30
20
|
end.join("\n")}
|
31
21
|
HEREDOC
|
32
22
|
end
|
33
23
|
|
34
24
|
private
|
35
25
|
|
26
|
+
def parse_cert(cert)
|
27
|
+
name_portion = cert[0].ljust(longest_cert_name_length + 6, ' ')
|
28
|
+
status_sym = cert_status(cert[1])
|
29
|
+
status = cert_status_strings[status_sym].to_s
|
30
|
+
colorized_status = status.colorize(cert_status_colors[status_sym])
|
31
|
+
date_portion = cert[1].strftime('%e %b %Y %H:%M:%S%p')
|
32
|
+
" #{name_portion} #{colorized_status} #{date_portion}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def longest_cert_name_length
|
36
|
+
@results.map { |r| r[0].length }.max
|
37
|
+
end
|
38
|
+
|
39
|
+
def sorted_results
|
40
|
+
if @config['sort_method'] == 'alphabetical'
|
41
|
+
@results.sort_by { |c| c[0] }
|
42
|
+
elsif @config['sort_method'] == 'expiration'
|
43
|
+
@results.sort_by { |c| c[1] }
|
44
|
+
else # default to alphabetical
|
45
|
+
@results.sort_by { |c| c[0] }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
36
49
|
def cert_dates(certs)
|
37
|
-
|
50
|
+
certs.map do |name, path|
|
38
51
|
if File.exist?(path)
|
39
|
-
|
40
|
-
parsed = cmd_result.match(/notAfter=([\w\s:]+)\n/)
|
41
|
-
if parsed.nil?
|
42
|
-
@errors << ComponentError.new(self, 'Unable to find certificate expiration date')
|
43
|
-
else
|
44
|
-
begin
|
45
|
-
expiry_date = DateTime.parse(parsed[1])
|
46
|
-
[name, expiry_date]
|
47
|
-
rescue ArgumentError
|
48
|
-
@errors << ComponentError.new(self, 'Found expiration date, but unable to parse as date')
|
49
|
-
end
|
50
|
-
end
|
52
|
+
parse_result(name, path)
|
51
53
|
else
|
52
54
|
"Certificate #{name} not found at path: #{path}"
|
53
55
|
end
|
56
|
+
end.compact # remove nil entries, will have nil if error ocurred
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_result(name, path)
|
60
|
+
cmd_result = `openssl x509 -in #{path} -dates`
|
61
|
+
# 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/
|
63
|
+
parsed = cmd_result.match(exp)
|
64
|
+
|
65
|
+
if parsed.nil?
|
66
|
+
@errors << ComponentError.new(self, 'Unable to find certificate '\
|
67
|
+
'expiration date')
|
68
|
+
nil
|
69
|
+
else
|
70
|
+
expiry_date = Time.parse([1, 2, 4, 3, 5].map { |n| parsed[n] }.join(' '))
|
71
|
+
[name, expiry_date]
|
54
72
|
end
|
73
|
+
rescue ArgumentError
|
74
|
+
@errors << ComponentError.new(self, 'Found expiration date, but unable '\
|
75
|
+
'to parse as date')
|
76
|
+
[name, Time.now]
|
55
77
|
end
|
56
78
|
|
57
79
|
def cert_status(expiry_date)
|
58
|
-
if (
|
80
|
+
if (Time.now...Time.now + 30).cover? expiry_date # ... range excludes last
|
59
81
|
:expiring
|
60
|
-
elsif
|
82
|
+
elsif Time.now >= expiry_date
|
61
83
|
:expired
|
62
84
|
else
|
63
85
|
:valid
|
@@ -65,7 +87,7 @@ class SSLCertificates
|
|
65
87
|
end
|
66
88
|
|
67
89
|
def cert_status_colors
|
68
|
-
|
90
|
+
{
|
69
91
|
valid: :green,
|
70
92
|
expiring: :yellow,
|
71
93
|
expired: :red
|
@@ -73,7 +95,7 @@ class SSLCertificates
|
|
73
95
|
end
|
74
96
|
|
75
97
|
def cert_status_strings
|
76
|
-
|
98
|
+
{
|
77
99
|
valid: 'valid until',
|
78
100
|
expiring: 'expiring at',
|
79
101
|
expired: 'expired at'
|
@@ -1,14 +1,10 @@
|
|
1
1
|
require 'sysinfo'
|
2
2
|
|
3
|
-
class Uptime
|
4
|
-
attr_reader :name, :errors
|
3
|
+
class Uptime < Component
|
5
4
|
attr_reader :days, :hours, :minutes
|
6
5
|
|
7
6
|
def initialize(motd)
|
8
|
-
|
9
|
-
@motd = motd
|
10
|
-
@config = motd.config.component_config(@name)
|
11
|
-
@errors = []
|
7
|
+
super(motd, 'uptime')
|
12
8
|
end
|
13
9
|
|
14
10
|
def process
|
@@ -20,7 +16,7 @@ class Uptime
|
|
20
16
|
end
|
21
17
|
|
22
18
|
def to_s
|
23
|
-
|
19
|
+
"#{@config['prefix'] || 'up'} #{format_uptime}"
|
24
20
|
end
|
25
21
|
|
26
22
|
private
|
data/lib/panda_motd/config.rb
CHANGED
@@ -2,8 +2,10 @@ require 'yaml'
|
|
2
2
|
require 'fileutils'
|
3
3
|
|
4
4
|
class Config
|
5
|
+
attr_reader :file_path
|
6
|
+
|
5
7
|
def initialize(file_path = nil)
|
6
|
-
@file_path = file_path
|
8
|
+
@file_path = file_path || File.join(Dir.home, '.config', 'panda-motd.yaml')
|
7
9
|
unless File.exist?(@file_path)
|
8
10
|
create_config(@file_path)
|
9
11
|
puts "panda-motd created a default config file at: #{@file_path}"
|
@@ -12,31 +14,34 @@ class Config
|
|
12
14
|
end
|
13
15
|
|
14
16
|
def components_enabled
|
15
|
-
#
|
16
|
-
|
17
|
+
# iterate config hash and grab names of enabled components
|
18
|
+
enabled = @config['components'].map { |c, s| c if s['enabled'] }.compact
|
17
19
|
# get the class constant
|
18
|
-
|
20
|
+
enabled.map { |e| Config.component_classes[e.to_sym] }
|
19
21
|
end
|
20
22
|
|
21
23
|
def component_config(component_name)
|
22
|
-
|
24
|
+
@config['components'][component_name.to_s]
|
23
25
|
end
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
def component_classes
|
28
|
-
return {
|
27
|
+
def self.component_classes
|
28
|
+
{
|
29
29
|
ascii_text_art: ASCIITextArt,
|
30
30
|
service_status: ServiceStatus,
|
31
31
|
uptime: Uptime,
|
32
32
|
ssl_certificates: SSLCertificates,
|
33
33
|
filesystems: Filesystems,
|
34
|
-
last_login: LastLogin
|
34
|
+
last_login: LastLogin,
|
35
|
+
fail_2_ban: Fail2Ban
|
35
36
|
}
|
36
37
|
end
|
37
38
|
|
39
|
+
private
|
40
|
+
|
38
41
|
def create_config(file_path)
|
39
|
-
default_config_path = File.join(
|
42
|
+
default_config_path = File.join(
|
43
|
+
File.dirname(__dir__), 'panda_motd', 'default_config.yaml'
|
44
|
+
)
|
40
45
|
FileUtils.cp(default_config_path, file_path)
|
41
46
|
end
|
42
47
|
|
@@ -62,6 +62,10 @@ components:
|
|
62
62
|
# Displays the validity and expiration dates of SSL certificates.
|
63
63
|
#
|
64
64
|
# Settings
|
65
|
+
# sort_method: The method used to sort the list of certificates. If no
|
66
|
+
# method is specified, the list will be sorted alphabetically. Valid
|
67
|
+
# values are 'alphabetical' and 'expiration'. The latter will place
|
68
|
+
# the certificates which are most near expiration at the top.
|
65
69
|
# certs: Pairs following the format "PrettyName: absolute_cert_file_path".
|
66
70
|
# The absolute_cert_file_path is the absolute file path of the SSL
|
67
71
|
# certificate. The pretty name is the name that is used in the MOTD to
|
@@ -70,8 +74,9 @@ components:
|
|
70
74
|
|
71
75
|
# ssl_certificates:
|
72
76
|
# enabled: true
|
77
|
+
# sort_method: alphabetical
|
73
78
|
# certs:
|
74
|
-
#
|
79
|
+
# thurlow.io: /etc/letsencrypt/live/thurlow.io/cert.pem
|
75
80
|
|
76
81
|
#####
|
77
82
|
# Filesystems
|
@@ -90,6 +95,21 @@ components:
|
|
90
95
|
# /dev/sda1: Ubuntu
|
91
96
|
# /dev/sdc1: Data
|
92
97
|
|
98
|
+
#####
|
99
|
+
# Fail2Ban
|
100
|
+
# Displays fail2ban jail statistics.
|
101
|
+
#
|
102
|
+
# Settings
|
103
|
+
# jails: A list of fail2ban jails to obtain statistics from. The name of the
|
104
|
+
# jail is the same name used in the `fail2ban-client status jailname`. You
|
105
|
+
# will get the total banned and currently banned numbers for each jail.
|
106
|
+
#####
|
107
|
+
|
108
|
+
# fail_2_ban:
|
109
|
+
# enabled: true
|
110
|
+
# jails:
|
111
|
+
# - sshd
|
112
|
+
# - anotherjail
|
93
113
|
|
94
114
|
#####
|
95
115
|
# Last Login
|
data/lib/panda_motd/motd.rb
CHANGED
@@ -3,14 +3,14 @@ require 'sysinfo'
|
|
3
3
|
class MOTD
|
4
4
|
attr_reader :config, :components
|
5
5
|
|
6
|
-
def initialize(config_path = nil)
|
6
|
+
def initialize(config_path = nil, process = true)
|
7
7
|
@config = config_path ? Config.new(config_path) : Config.new
|
8
8
|
@components = @config.components_enabled.map { |ce| ce.new(self) }
|
9
|
-
@components.each(&:process)
|
9
|
+
@components.each(&:process) if process
|
10
10
|
end
|
11
11
|
|
12
12
|
def to_s
|
13
|
-
|
13
|
+
@components.map do |c|
|
14
14
|
if c.errors.any?
|
15
15
|
c.errors.map(&:to_s).join("\n")
|
16
16
|
else
|
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.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taylor Thurlow
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-11-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: artii
|
@@ -81,103 +81,131 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.8'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: factory_bot
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
96
|
+
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: guard
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
103
|
+
version: '0'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- - "
|
108
|
+
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
110
|
+
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: guard-rspec
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- - "
|
115
|
+
- - ">="
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
117
|
+
version: '0'
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- - "
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: pry-byebug
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
123
151
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
152
|
+
version: '0'
|
125
153
|
- !ruby/object:Gem::Dependency
|
126
154
|
name: rspec
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
128
156
|
requirements:
|
129
|
-
- - "
|
157
|
+
- - ">="
|
130
158
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
159
|
+
version: '0'
|
132
160
|
type: :development
|
133
161
|
prerelease: false
|
134
162
|
version_requirements: !ruby/object:Gem::Requirement
|
135
163
|
requirements:
|
136
|
-
- - "
|
164
|
+
- - ">="
|
137
165
|
- !ruby/object:Gem::Version
|
138
|
-
version: '
|
166
|
+
version: '0'
|
139
167
|
- !ruby/object:Gem::Dependency
|
140
168
|
name: rubocop
|
141
169
|
requirement: !ruby/object:Gem::Requirement
|
142
170
|
requirements:
|
143
|
-
- - "
|
171
|
+
- - ">="
|
144
172
|
- !ruby/object:Gem::Version
|
145
|
-
version: '0
|
173
|
+
version: '0'
|
146
174
|
type: :development
|
147
175
|
prerelease: false
|
148
176
|
version_requirements: !ruby/object:Gem::Requirement
|
149
177
|
requirements:
|
150
|
-
- - "
|
178
|
+
- - ">="
|
151
179
|
- !ruby/object:Gem::Version
|
152
|
-
version: '0
|
180
|
+
version: '0'
|
153
181
|
- !ruby/object:Gem::Dependency
|
154
182
|
name: rubocop-rspec
|
155
183
|
requirement: !ruby/object:Gem::Requirement
|
156
184
|
requirements:
|
157
|
-
- - "
|
185
|
+
- - ">="
|
158
186
|
- !ruby/object:Gem::Version
|
159
|
-
version: '
|
187
|
+
version: '0'
|
160
188
|
type: :development
|
161
189
|
prerelease: false
|
162
190
|
version_requirements: !ruby/object:Gem::Requirement
|
163
191
|
requirements:
|
164
|
-
- - "
|
192
|
+
- - ">="
|
165
193
|
- !ruby/object:Gem::Version
|
166
|
-
version: '
|
194
|
+
version: '0'
|
167
195
|
- !ruby/object:Gem::Dependency
|
168
196
|
name: simplecov
|
169
197
|
requirement: !ruby/object:Gem::Requirement
|
170
198
|
requirements:
|
171
|
-
- - "
|
199
|
+
- - ">="
|
172
200
|
- !ruby/object:Gem::Version
|
173
|
-
version: '0
|
201
|
+
version: '0'
|
174
202
|
type: :development
|
175
203
|
prerelease: false
|
176
204
|
version_requirements: !ruby/object:Gem::Requirement
|
177
205
|
requirements:
|
178
|
-
- - "
|
206
|
+
- - ">="
|
179
207
|
- !ruby/object:Gem::Version
|
180
|
-
version: '0
|
208
|
+
version: '0'
|
181
209
|
description: Enhance your MOTD with useful at-a-glance information.
|
182
210
|
email: taylorthurlow8@gmail.com
|
183
211
|
executables:
|
@@ -187,8 +215,10 @@ extra_rdoc_files: []
|
|
187
215
|
files:
|
188
216
|
- bin/panda-motd
|
189
217
|
- lib/panda_motd.rb
|
218
|
+
- lib/panda_motd/component.rb
|
190
219
|
- lib/panda_motd/component_error.rb
|
191
220
|
- lib/panda_motd/components/ascii_text_art.rb
|
221
|
+
- lib/panda_motd/components/fail_2_ban.rb
|
192
222
|
- lib/panda_motd/components/filesystems.rb
|
193
223
|
- lib/panda_motd/components/last_login.rb
|
194
224
|
- lib/panda_motd/components/service_status.rb
|
@@ -218,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
218
248
|
version: '0'
|
219
249
|
requirements: []
|
220
250
|
rubyforge_project:
|
221
|
-
rubygems_version: 2.7.
|
251
|
+
rubygems_version: 2.7.8
|
222
252
|
signing_key:
|
223
253
|
specification_version: 4
|
224
254
|
summary: Make your MOTD prettier, and more useful.
|