Skip to content

Instantly share code, notes, and snippets.

@pvdb
Last active September 15, 2021 00:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pvdb/ff6f50b1800e61d33f02 to your computer and use it in GitHub Desktop.
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
#!/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!
#!/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.'
@pvdb
Copy link
Author

pvdb commented Mar 26, 2015

@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

@pvdb
Copy link
Author

pvdb commented Mar 26, 2015

@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

@pvdb
Copy link
Author

pvdb commented Aug 13, 2015

@pdmlester: see above comments ^^^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment