Useful CLI tools (bash) for Arch Linux administration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

482 lines
14 KiB

4 months ago
6 years ago
6 years ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
4 months ago
4 months ago
4 months ago
6 years ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
6 years ago
4 months ago
4 months ago
4 months ago
4 months ago
6 years ago
4 months ago
4 months ago
  1. #!/usr/bin/env bash
  2. #
  3. # archrisks - Get security risk severity & count of installed packages on Arch Linux
  4. # Copyright (C) 2021,2024 Pekka Helenius
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. #####################################
  19. red=$'\33[91m'
  20. orange=$'\033[38;5;208m'
  21. yellow=$'\033[93m'
  22. green=$'\033[92m'
  23. reset=$'\033[0m'
  24. typeset -A ARCH_MANAGERS
  25. # List of known Arch Linux package managers + priority number for each
  26. # Add more if you wish.
  27. # Syntax guidelines:
  28. # [<package manager priority (greater is more prioritized)>,<package manager>]="
  29. # <parameter to refresh local pkg repositories>|
  30. # <parameter to get detailed local pkg info>|
  31. # <parameter to get name and version string of a pkg>|
  32. # <parameter to get detailed remote pkg info>|
  33. # <root=requires root, no_root=does not require root>
  34. # "
  35. ARCH_MANAGERS=(
  36. [0,/usr/bin/pacman]="-Syy|-Qi|-Q|-Si|root"
  37. [1,/usr/bin/pacaur]="-Syy|-Qi|-Q|-Si|no_root"
  38. # [2,yaourt]="-Syy|-Qi|-Q|-Si|no_root"
  39. # [3,pikaur]="-Syy|-Qi|-Q|-Si|no_root"
  40. # [4,pacget]="-Syy|-Qi|-Q|-Si|no_root"
  41. # [5,yay]="-Syy|-Qi|-Q|-Si|no_root"
  42. # [6,foxaur]="-Syy|-Qi|-Q|-Si|no_root"
  43. # [7,aurum]="-Syy|-Qi|-Q|-Si|no_root"
  44. # [8,goaur]="-Syy|-Qi|-Q|-Si|no_root"
  45. # [9,aurs]="-Syy|-Qi|-Q|-Si|no_root"
  46. # [10,magico]="-Syy|-Qi|-Q|-Si|no_root"
  47. # [11,maur]="-Syy|-Qi|-Q|-Si|no_root"
  48. # [12,pkgbuilder]="-Syy|-Qi|-Q|-Si|no_root"
  49. # [13,spinach]="-Syy|-Qi|-Q|-Si|no_root"
  50. # [14,trizen]="-Syy|-Qi|-Q|-Si|no_root"
  51. )
  52. SELECTED_MANAGER=
  53. MANAGER_PRIORITY_LOWLIMIT=-1
  54. SORT_ORDER="level"
  55. SORT_REVERSE=0
  56. NETWORK_HOST_ENDPOINT="security.archlinux.org"
  57. input_count=${#@}
  58. [[ "${input_count}" -eq 1 ]] && input_1="${1}"
  59. [[ "${input_count}" -eq 2 ]] && input_1="${1}"; input_2="${2}"
  60. usage() {
  61. echo -e "
  62. Usage: $0
  63. -h|--help
  64. 1st arg: --sort=<name,issues,level,status,desc> (optional)
  65. 2nd arg: --reverse (optional)
  66. "
  67. exit 0
  68. }
  69. input_parser() {
  70. if \
  71. [[ "${input_count}" -gt 2 ]] || \
  72. [[ "${input_1}" == "-h" ]] || \
  73. [[ "${input_1}" == "--help" ]]
  74. then
  75. usage
  76. elif [[ "${input_count}" -ne 0 ]]
  77. then
  78. SORT_ORDER=$(echo "${input_1}" | sed -r 's/^\-\-sort=(.*)/\1/')
  79. case "${SORT_ORDER}" in
  80. name|issues|level|version|desc)
  81. echo "Custom sort order selected: ${SORT_ORDER}"
  82. ;;
  83. *)
  84. echo "Unknown sorting order selected (${SORT_ORDER})."
  85. usage
  86. esac
  87. if [[ "${input_count}" -eq 2 ]]
  88. then
  89. case "${input_2}" in
  90. "--reverse")
  91. echo "Reverse ordering"
  92. SORT_REVERSE=1
  93. ;;
  94. *)
  95. echo "Unknown option '${input_2}'"
  96. SORT_REVERSE=0
  97. esac
  98. fi
  99. fi
  100. }
  101. connection_test() {
  102. local host_endpoint
  103. host_endpoint="${1}"
  104. if [[ $(ping -c 1 "${host_endpoint}" 2>&1 | grep -c "Name or service not known") -ne 0 ]]
  105. then
  106. echo -e "\nCan't connect to $host_endpoint. Please check your internet connection and try again.\n"
  107. exit 0
  108. fi
  109. }
  110. function find_my_package_manager() {
  111. local i
  112. local managers_list
  113. local managers_priority_list
  114. local manager_priority
  115. local manager
  116. i=0
  117. managers_list=()
  118. managers_priority_list=()
  119. for manager_str in ${!ARCH_MANAGERS[@]}; do
  120. OLDIFS=${IFS}
  121. IFS=","
  122. manager_array=(${manager_str})
  123. IFS=${OLDIFS}
  124. manager_priority="${manager_array[0]}"
  125. manager="${manager_array[1]}"
  126. if [[ "${manager_priority}" -lt "${MANAGER_PRIORITY_LOWLIMIT}" ]]
  127. then
  128. echo "Minimum priority is $((${MANAGER_PRIORITY_LOWLIMIT} + 1)). You have a package which has lower priority value. Exiting."
  129. exit 1
  130. fi
  131. if [[ $(type -P ${manager}) ]]
  132. then
  133. managers_list[$i]="${manager}"
  134. managers_priority_list[$i]=${manager_priority}
  135. let i++
  136. fi
  137. done
  138. if [[ ${#managers_list[@]} -eq 0 ]]
  139. then
  140. echo "Not any valid package manager found. Exiting."
  141. exit 1
  142. fi
  143. if [[ $(echo ${managers_priority_list[@]} | tr ' ' '\n' | uniq -d | wc -l) -ne 0 ]]
  144. then
  145. echo "Package managers with same priority found. Check internal manager list for duplicates. Exiting."
  146. exit 1
  147. fi
  148. # Select package manager by priority. Highest is selected.
  149. i=0
  150. while [[ "${i}" -le $((${#managers_list[@]} - 1)) ]]
  151. do
  152. if [[ ${managers_priority_list[i]} -gt ${priority_lowlimit} ]]
  153. then
  154. priority_lowlimit=${managers_priority_list[i]}
  155. SELECTED_MANAGER=${managers_list[i]}
  156. fi
  157. let i++
  158. done
  159. OLDIFS=${IFS}
  160. IFS="|"
  161. pkg_command=(${ARCH_MANAGERS["$priority_lowlimit,$SELECTED_MANAGER"]})
  162. IFS=${OLDIFS}
  163. command_refresh="${pkg_command[0]}"
  164. command_pkginfo_local="${pkg_command[1]}"
  165. command_pkginfo_local_short="${pkg_command[2]}"
  166. command_pkginfo_remote="${pkg_command[3]}"
  167. command_require_root="${pkg_command[4]}"
  168. if [[ "${command_require_root}" == "root" ]]
  169. then
  170. if [[ ! $(id -u) -eq 0 ]]
  171. then
  172. echo -e "\nThis command requires root privileges.\n"
  173. exit 0
  174. fi
  175. fi
  176. }
  177. # TODO: We can't really depend on parsing output strings since they vary between Arch package managers
  178. package_version_parsed() {
  179. echo "${1}" | awk -F ' ' '{print $2}' | sed -r 's/[a-z]+.*//; s/[:_+-]/\./g; s/[^0-9]$//;'
  180. }
  181. package_version_check() {
  182. local system_version
  183. local repo_version
  184. local version_array_1
  185. local version_array_2
  186. local first_version_numbers
  187. local last_version_numbers
  188. local comparables
  189. local version_status_msg
  190. local check1
  191. local check2
  192. local s
  193. # Expected output syntax: "^<string> <version number>$"
  194. # TODO: We can't really depend on parsing output strings since they vary between Arch package managers
  195. system_version=$(${SELECTED_MANAGER} ${command_pkginfo_local_short} "${1}")
  196. repo_version=$(${SELECTED_MANAGER} ${command_pkginfo_remote} $1 | grep -E "^Version\s*:" | sed -r 's/.*(:\s*.*$)/\1/')
  197. version_array_1=$(package_version_parsed "${system_version}")
  198. version_array_2=$(package_version_parsed "${repo_version}")
  199. #Count of version elements (0 18 2 1 contains 4 numbers, for example)
  200. first_version_numbers=$(echo "${version_array_1}" | awk -F '.' '{print split($0, a)}')
  201. last_version_numbers=$(echo "${version_array_2}" | awk -F '.' '{print split($0, a)}')
  202. # Count of comparable version elements (maximum)
  203. # We compare this much of elements, not more
  204. if [[ "${last_version_numbers}" -lt "${first_version_numbers}" ]]
  205. then
  206. comparables="${last_version_numbers}"
  207. else
  208. comparables="${first_version_numbers}"
  209. fi
  210. # If all numbers are same, we don't analyze them more deeply.
  211. if [[ "${version_array_1}" == "${version_array_2}" ]]
  212. then
  213. version_status_msg="${green}Package is updated"
  214. else
  215. s=1
  216. while [ ${s} -le ${comparables} ]
  217. do
  218. check1=$(echo -e "${version_array_1}" | awk -v var=$s -F '.' '{print $var}')
  219. check2=$(echo -e "${version_array_2}" | awk -v var=$s -F '.' '{print $var}')
  220. if [[ ${check2} -gt ${check1} ]]
  221. then
  222. # Repo number is greater
  223. version_status_msg="${yellow}Update available"
  224. break
  225. elif [[ ${check2} -lt ${check1} ]]
  226. then
  227. # System number is greater
  228. version_status_msg="${reset}Newer package installed"
  229. break
  230. fi
  231. let s++
  232. done
  233. fi
  234. if [[ -z "${version_status_msg}" ]]
  235. then
  236. version_status_msg="${reset}Unknown"
  237. fi
  238. echo "${version_status_msg}"
  239. }
  240. exec_tool() {
  241. local i
  242. local package_count
  243. local risks_parsed_count
  244. local risks
  245. local description_column_max_chars
  246. local r_package_name
  247. local r_package_security_issues_count
  248. local r_package_security_issues_level
  249. local r_package_security_issues_level
  250. local r_package_description
  251. local risk_entries
  252. local sort_params
  253. local sort_column
  254. local package_alert_importance_status
  255. local package_alert_msg_color
  256. local package_alert_importance_output_status
  257. local package_security_issues_count
  258. local security_issues_package_summary
  259. local security_issues_total_count
  260. local security_msg_color
  261. i=1
  262. description_column_max_chars=35
  263. sort_params=()
  264. input_parser
  265. connection_test "${NETWORK_HOST_ENDPOINT}"
  266. find_my_package_manager
  267. echo "Security report date: $(date '+%d-%m-%Y, %X') (TZ: $(timedatectl status | grep "Time zone:" | awk '{print $3}'))"
  268. echo -e "\nSynchronizing package databases with ${SELECTED_MANAGER}\n"
  269. ${SELECTED_MANAGER} ${command_refresh} || exit
  270. if [[ ! $(type -P arch-audit) ]]
  271. then
  272. echo -e "\nCouldn't find Arch Linux security utility (arch-audit) in \$PATH. Please make sure it's installed.\n"
  273. else
  274. packages=
  275. package_count=0
  276. risks_parsed_count=0
  277. IFS=$'\n'
  278. for au in $(arch-audit); do
  279. package_name=$(echo "${au}" | awk -F ' ' '{print $1}')
  280. risk_level=$(echo "${au}" | grep -oE "Low|Medium|High|Critical")
  281. risks_count=$(echo "${au}" | grep -oP "(?<=by ).+(?=\. )" | sed 's/, /\n/g' | wc -l)
  282. risks[$package_count]="$package_name;$risk_level;$risks_count"
  283. packages="${packages}, ${package_name}"
  284. let package_count++
  285. done
  286. echo -e "Analyzing ${#risks[*]} vulnerable packages. This takes a while...\n"
  287. echo -e "Vulnerable packages are:\n\n$(echo ${packages} | sed 's/^, //')\n"
  288. for risk_parsed in ${risks[@]}; do
  289. OLDIFS=${IFS}
  290. IFS=";"
  291. risk_parsed=(${risk_parsed})
  292. IFS=${OLDIFS}
  293. # Package name
  294. r_package_name="${risk_parsed[0]}"
  295. echo -en "Analysing package ${i}/${#risks[*]} (${r_package_name})... \r"
  296. # Package security issues detected
  297. r_package_security_issues_count="${risk_parsed[2]}"
  298. # Package security issues overall level: Critical, High, Medium or Low
  299. r_package_security_issues_level=$(echo "${risk_parsed[1]}" | sed 's/Critical/0/g; s/High/1/g; s/Medium/2/g; s/Low/3/g')
  300. r_package_description=$(${SELECTED_MANAGER} "${command_pkginfo_local}" "${r_package_name}" | grep -i description | awk -F ": " '{print $2}')
  301. if [[ $(echo "${r_package_description}" | wc -m) -gt ${description_column_max_chars} ]]
  302. then
  303. r_package_description=$(printf "%s..." $(echo "${r_package_description}" | cut -c 1-${description_column_max_chars}))
  304. fi
  305. r_package_version_status=$(package_version_check "${r_package_name}")
  306. risk_entries[$risks_parsed_count]=$(printf "%s|%s|%s|%s|%s\n" \
  307. "${r_package_name}" \
  308. "${r_package_security_issues_count}" \
  309. "${r_package_security_issues_level}" \
  310. "${r_package_version_status}" \
  311. "${r_package_description}" \
  312. )
  313. let risks_parsed_count++
  314. let i++
  315. done
  316. echo -e "\e[1m"
  317. printf "\n%-25s%-20s%-15s%-25s%s\n" "Package" "Security issues" "Risk level" "Version status" "Description"
  318. echo -e "\e[0m"
  319. case "${SORT_ORDER}" in
  320. name)
  321. sort_column="-k1"
  322. ;;
  323. issues)
  324. sort_column="-k2"
  325. sort_params+=("-n")
  326. ;;
  327. level)
  328. sort_column="-k3"
  329. ;;
  330. version)
  331. sort_column="-k4"
  332. ;;
  333. desc)
  334. sort_column="-k5"
  335. ;;
  336. #*)
  337. # echo "Unknown sorting order selected. Exiting."
  338. # exit 1
  339. esac
  340. if [[ "${SORT_REVERSE}" -eq 1 ]]
  341. then
  342. sort_params+=("-r")
  343. fi
  344. i=0
  345. IFS=$'\n'
  346. for line in $(echo "${risk_entries[*]}" | sort ${sort_params[*]} -t'|' ${sort_column})
  347. do
  348. package_alert_importance_status=$(echo "${line}" | awk -F '|' '{print $3}')
  349. case "${package_alert_importance_status}" in
  350. 0)
  351. package_alert_msg_color="${red}"
  352. package_alert_importance_output_status="Critical"
  353. ;;
  354. 1)
  355. package_alert_msg_color="${orange}"
  356. package_alert_importance_output_status="High"
  357. ;;
  358. 2)
  359. package_alert_msg_color="${yellow}"
  360. package_alert_importance_output_status="Medium"
  361. ;;
  362. 3)
  363. package_alert_msg_color="${green}"
  364. package_alert_importance_output_status="Low"
  365. ;;
  366. esac
  367. package_security_issues_count=$(echo "${line}" | awk -F '|' '{print $2}')
  368. if [[ ${package_security_issues_count} -lt 5 ]]
  369. then
  370. security_msg_color="${green}"
  371. elif [[ ${package_security_issues_count} -ge 5 ]] && [[ ${package_security_issues_count} -lt 10 ]]
  372. then
  373. security_msg_color="${yellow}"
  374. elif [[ ${package_security_issues_count} -ge 10 ]] && [[ ${package_security_issues_count} -lt 20 ]]
  375. then
  376. security_msg_color="${orange}"
  377. else
  378. security_msg_color="${red}"
  379. fi
  380. security_issues_package_summary[$i]=${package_security_issues_count}
  381. echo "${line}" | awk -F '|' \
  382. -v clr1="${package_alert_msg_color}" \
  383. -v clr2="${security_msg_color}" \
  384. -v rs="${reset}" \
  385. -v var="${package_alert_importance_output_status}" \
  386. '{printf "%-25s%s%-20s%s%-15s%-30s%s%s\n",$1,clr2,$2,clr1,var,$4,rs,$5}'
  387. let i++
  388. done
  389. security_issues_total_count=$(echo $(printf "%d+" "${security_issues_package_summary[@]}")0 | bc)
  390. echo -e "\nTotal:"
  391. printf "%-25s%s\n\n" "${package_count}" "${security_issues_total_count}"
  392. printf "Check %s for more information.\n\n" "${NETWORK_HOST_ENDPOINT}"
  393. fi
  394. }
  395. exec_tool