Last active
September 15, 2021 00:22
-
-
Save pvdb/ff6f50b1800e61d33f02 to your computer and use it in GitHub Desktop.
determine and print out the age of the oldest and the age of the most recent security update
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# | |
# This script can be used to patch the following script on an Amazon Linux AMI: | |
# | |
# /etc/update-motd.d/70-available-updates | |
# | |
# which is run by cron on a daily basis to add package update info to the MOTD: | |
# | |
# 19 package(s) needed for security, out of 160 available | |
# (oldest: 14 days and 12 hours old, most recent: 6 days and 15 hours old) | |
# Run "sudo yum update" to apply all updates. | |
# | |
# This script can be used to add the middle line in the above example containing | |
# the age of both the oldest as well as the most recent security update... more | |
# generally it provides the foundation logic for gathering patch latency data! | |
# | |
# Caveat: the script uses a package's BUILDTIME as a proxy for its release time | |
# (i.e. availability in an RPM repo) so the calculated age is not 100% accurate, | |
# but likely more accurate than the other RPM timestamps (COMMITTIME & FILETIME) | |
# | |
# Examples: | |
# | |
# # run as executable: | |
# ./70-available-updates-incl-age.rb | |
# | |
# # use as library: | |
# ruby -I . -r 70-available-updates-incl-age.rb -e 'puts SECURITY_UPDATES' | |
# | |
RPM = Struct.new(:name, :package_name, :buildtime, :age, :age_in_words) | |
def duration(age) | |
secs = age.to_int | |
mins = secs / 60 | |
hours = mins / 60 | |
days = hours / 24 | |
if days > 0 | |
(hours % 24).zero? ? "#{days} days" : "#{days} days and #{hours % 24} hours" | |
elsif hours > 0 | |
(mins % 60).zero? ? "#{hours} hours" : "#{hours} hours and #{mins % 60} minutes" | |
elsif mins > 0 | |
(secs % 60).zero? ? "#{mins} minutes" : "#{mins} minutes and #{secs % 60} seconds" | |
elsif secs >= 0 | |
"#{secs} seconds" | |
end | |
end | |
SECURITY_UPDATES = `/usr/bin/yum --debuglevel 0 --security check-update` | |
.split($/) # break up multi-line output | |
.delete_if(&:empty?) # skip empty lines in output | |
.take_while { |line| # skip "obsoleting" lines... | |
line !~ /\AObsoleting Packages\z/ } | |
.map { |line| # convert lines to instances | |
name_dot_arch, version, _ = line.split(/\s+/) | |
name, _, arch = name_dot_arch.rpartition('.') | |
package_name = "#{name}-#{version}.#{arch}" # convert into RPM-friendly package names | |
buildtime = `repoquery --query --qf='%{buildtime}' #{package_name}` # determine BUILDTIME of each package | |
buildtime = Time.at(buildtime.chomp.to_i) # raise exception of buildtime invalid | |
age = Time.now.to_i - buildtime.to_i # determine age of each package in seconds | |
age_in_words = duration(age) + " old" # determine age of each package in words | |
RPM.new(name, package_name, buildtime, age, age_in_words) | |
} | |
def SECURITY_UPDATES.oldest | |
sort_by(&:age).last | |
end | |
def SECURITY_UPDATES.youngest | |
sort_by(&:age).first | |
end | |
if $0 == __FILE__ | |
class String | |
def colorize(color_code) | |
if STDOUT.tty? && STDERR.tty? | |
"\e[#{color_code}m#{self}\e[0m" | |
else | |
self | |
end | |
end | |
def invert() colorize('7') ; end | |
def bold() colorize('1') ; end | |
def red() colorize('31') ; end | |
end | |
if SECURITY_UPDATES.any? | |
puts "(oldest: %s, most recent: %s)" % [ | |
SECURITY_UPDATES.oldest.age_in_words.bold, | |
SECURITY_UPDATES.youngest.age_in_words.bold, | |
] | |
end | |
end | |
# That's all, Folks! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Possible summaries include: | |
# No packages needed for security; %d packages available | |
# %d package(s) needed[ (+%d related)] for security, out of %d available | |
# There are [[%d security update(s)[ out of ]%d total update(s)]] available | |
LANG=C /usr/bin/yum \ | |
--debuglevel 2 \ | |
--security check-update 2>/dev/null \ | |
| grep -P '(?<! 0 packages) available$' \ | |
&& ./70-available-updates-incl-age.rb \ | |
&& echo 'Run "sudo yum update" to apply all updates.' |
@waynemoore: might be a useful snippet to calculate the average age of security updates...
puts duration(SECURITY_UPDATES.map(&:age).instance_eval { reduce(:+) / size.to_f })
e.g. on one of our AERIE instances, with 19 security updates available(!), this results in:
11 days and 8 hours
FYI
@pdmlester: see above comments ^^^^
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@waynemoore: you can run something like this on each and every of our AWS instances...
curl -s https://gist.githubusercontent.com/pvdb/ff6f50b1800e61d33f02/raw/70-available-updates-incl-age.rb | ruby
FYI