check_gpg_expiry 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/python3
  2. # Copyright (C) 2021 Philipp Fromme
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import argparse
  17. import os
  18. import re
  19. import subprocess
  20. import sys
  21. import time
  22. from enum import Enum
  23. class State(Enum):
  24. OK = 0
  25. WARNING = 1
  26. CRITICAL = 2
  27. UNKNOWN = 3
  28. def match_regex(string, regex_list):
  29. for regex in regex_list:
  30. r = re.compile(regex)
  31. if bool(r.search(string)):
  32. return True
  33. return False
  34. def is_pgp(file):
  35. output = subprocess.check_output(["/usr/bin/file", "-b", "--mime-type", file])
  36. output = output.decode().rstrip()
  37. pgp_types = ["application/pgp-keys", "application/x-gnupg-keyring"]
  38. if output in pgp_types:
  39. return True
  40. return False
  41. def get_file_list(directory):
  42. path_list = []
  43. if not os.path.isdir(directory):
  44. return None
  45. for dirpath, dirnames, filenames in os.walk(directory):
  46. for f in filenames:
  47. file_path = os.path.join(dirpath, f)
  48. if is_pgp(file_path):
  49. path_list.append(file_path)
  50. return path_list
  51. def parse_gpg(output):
  52. lines = output.split('\n')
  53. keys = {}
  54. for line in lines:
  55. elements = line.split(':')
  56. # see https://github.com/gpg/gnupg/blob/master/doc/DETAILS
  57. # for colon listings format
  58. record = elements[0]
  59. if record == 'pub':
  60. added = False
  61. length = elements[2]
  62. algorithm = elements[3]
  63. issue_date = elements[5]
  64. expiry_date = elements[6]
  65. if expiry_date:
  66. expiry_date = int(expiry_date)
  67. else:
  68. expiry_date = None
  69. elif record == 'uid':
  70. userid = elements[9]
  71. if not added:
  72. keys[userid] = (length, algorithm, issue_date, expiry_date)
  73. added = True
  74. return keys
  75. def check_expiry(key, warning, critical):
  76. current_time = int(time.time())
  77. state = State.OK
  78. expiry_date = key[3]
  79. if expiry_date:
  80. expired = expiry_date - current_time
  81. if expired <= critical:
  82. state = State.CRITICAL
  83. elif expired <= warning:
  84. state = State.WARNING
  85. return state
  86. def verbose_print(details):
  87. hr_format = "%d-%m-%Y"
  88. for uid, fields in details.items():
  89. if len(details) > 1:
  90. print(" {}:".format(uid))
  91. print(" Algorithm: {}".format(fields[1]))
  92. print(" Length: {}".format(fields[0]))
  93. issue_date = int(fields[2])
  94. issue_date_hr = time.strftime(hr_format, time.localtime(issue_date))
  95. print(" Issue Date: {} - {}".format(issue_date, issue_date_hr))
  96. expiry_date = fields[3]
  97. if expiry_date:
  98. expiry_date_hr = time.strftime(hr_format, time.localtime(expiry_date))
  99. expiry_state = "{} - {}".format(expiry_date, expiry_date_hr)
  100. else:
  101. expiry_state = "None"
  102. print(" Expiry Date: {}".format(expiry_state))
  103. def main():
  104. parser = argparse.ArgumentParser(description="Check for expiring pgp keys")
  105. parser.add_argument("-w", "--warning", help="Warning threshold, default "
  106. "604800 seconds (one week)", nargs="?", type=int,
  107. default=604800)
  108. parser.add_argument("-c", "--critical", help="Critical threshold, default 0 seconds",
  109. nargs="?", type=int, default=0)
  110. parser.add_argument("-d", "--dirs", help="Directories to check for pgp keys in",
  111. nargs='+', default=["/etc/apt/trusted.gpg.d/"])
  112. parser.add_argument("-s", "--sort", help="Sort by expiry date, "
  113. "sorts by path otherwise", action="store_true")
  114. parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
  115. parser.add_argument("-e", "--expiring", help="Only show expiring keys, "
  116. "requires -v/--verbose to actually do something", action="store_true")
  117. parser.add_argument("-i", "--ignore", help="Regular expressions separated "
  118. "by space matching file paths to ignore", nargs="*")
  119. args = parser.parse_args()
  120. state = State.OK
  121. crit_list = []
  122. warn_list = []
  123. unkn_list = []
  124. err_list = []
  125. files = []
  126. verbose_info = {}
  127. closest_expiry = {}
  128. for directory in args.dirs:
  129. file_list = get_file_list(directory)
  130. if file_list is None:
  131. state = State.UNKNOWN
  132. unkn_list.append(directory)
  133. continue
  134. files = [*files, *file_list]
  135. files = set(files)
  136. for file_path in files:
  137. if args.ignore:
  138. if match_regex(file_path, args.ignore):
  139. continue
  140. try:
  141. command = ["/usr/bin/gpg", "--dry-run", "--import-options",
  142. "import-show", "--import", "--batch", "--quiet",
  143. "--no-keyring", "--trust-model", "always",
  144. "--with-colons", file_path]
  145. output = subprocess.check_output(command, stderr=subprocess.STDOUT)
  146. except subprocess.CalledProcessError as e:
  147. unkn_list.append(file_path)
  148. err_list.append(e.output.decode().rstrip())
  149. state = State.UNKNOWN
  150. continue
  151. keys = parse_gpg(output.decode())
  152. for key in keys:
  153. expiry_state = check_expiry(keys[key], args.warning, args.critical)
  154. if expiry_state == State.CRITICAL:
  155. if state != State.UNKNOWN:
  156. state = expiry_state
  157. crit_list.append(file_path)
  158. elif expiry_state == State.WARNING:
  159. if state == State.OK:
  160. state = expiry_state
  161. warn_list.append(file_path)
  162. if args.verbose:
  163. verbose_info[file_path] = keys
  164. for key in keys:
  165. expiry_date = keys[key][3]
  166. if not expiry_date:
  167. expiry_date = 3000000000
  168. if file_path in closest_expiry:
  169. if closest_expiry[file_path] > expiry_date:
  170. closest_expiry[file_path] = expiry_date
  171. else:
  172. closest_expiry[file_path] = expiry_date
  173. output = "{} -".format(state.name)
  174. if crit_list:
  175. output += " Critical: [ {} ]".format(", ".join(x for x in crit_list))
  176. if warn_list:
  177. output += " Warning: [ {} ]".format(", ".join(x for x in warn_list))
  178. if unkn_list:
  179. output += " Unknown: [ {} ]".format(", ".join(x for x in unkn_list))
  180. if err_list:
  181. output += " Error: [ {} ]".format(", ".join(x for x in err_list))
  182. if not (crit_list or warn_list or unkn_list or err_list):
  183. output += " All keys ok"
  184. print(output)
  185. if args.verbose:
  186. sorted_keys = sorted(verbose_info.keys(), key=lambda file_path:
  187. (closest_expiry[file_path], file_path)
  188. if args.sort else file_path)
  189. for file_path in sorted_keys:
  190. cat_tupl = (*crit_list, *warn_list, *unkn_list)
  191. if file_path in cat_tupl or not args.expiring:
  192. print()
  193. print("{}:".format(file_path))
  194. verbose_print(verbose_info[file_path])
  195. sys.exit(state.value)
  196. if __name__ == "__main__":
  197. main()