#!/usr/bin/env bash # # archrisks - Get security risk severity & count of installed packages on Arch Linux # Copyright (C) 2021,2024 Pekka Helenius # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ##################################### red=$'\33[91m' orange=$'\033[38;5;208m' yellow=$'\033[93m' green=$'\033[92m' reset=$'\033[0m' typeset -A ARCH_MANAGERS # List of known Arch Linux package managers + priority number for each # Add more if you wish. # Syntax guidelines: # [,]=" # | # | # | # | # # " ARCH_MANAGERS=( [0,/usr/bin/pacman]="-Syy|-Qi|-Q|-Si|root" [1,/usr/bin/pacaur]="-Syy|-Qi|-Q|-Si|no_root" # [2,yaourt]="-Syy|-Qi|-Q|-Si|no_root" # [3,pikaur]="-Syy|-Qi|-Q|-Si|no_root" # [4,pacget]="-Syy|-Qi|-Q|-Si|no_root" # [5,yay]="-Syy|-Qi|-Q|-Si|no_root" # [6,foxaur]="-Syy|-Qi|-Q|-Si|no_root" # [7,aurum]="-Syy|-Qi|-Q|-Si|no_root" # [8,goaur]="-Syy|-Qi|-Q|-Si|no_root" # [9,aurs]="-Syy|-Qi|-Q|-Si|no_root" # [10,magico]="-Syy|-Qi|-Q|-Si|no_root" # [11,maur]="-Syy|-Qi|-Q|-Si|no_root" # [12,pkgbuilder]="-Syy|-Qi|-Q|-Si|no_root" # [13,spinach]="-Syy|-Qi|-Q|-Si|no_root" # [14,trizen]="-Syy|-Qi|-Q|-Si|no_root" ) SELECTED_MANAGER= MANAGER_PRIORITY_LOWLIMIT=-1 SORT_ORDER="level" SORT_REVERSE=0 NETWORK_HOST_ENDPOINT="security.archlinux.org" input_count=${#@} [[ "${input_count}" -eq 1 ]] && input_1="${1}" [[ "${input_count}" -eq 2 ]] && input_1="${1}"; input_2="${2}" usage() { echo -e " Usage: $0 -h|--help 1st arg: --sort= (optional) 2nd arg: --reverse (optional) " exit 0 } input_parser() { if \ [[ "${input_count}" -gt 2 ]] || \ [[ "${input_1}" == "-h" ]] || \ [[ "${input_1}" == "--help" ]] then usage elif [[ "${input_count}" -ne 0 ]] then SORT_ORDER=$(echo "${input_1}" | sed -r 's/^\-\-sort=(.*)/\1/') case "${SORT_ORDER}" in name|issues|level|version|desc) echo "Custom sort order selected: ${SORT_ORDER}" ;; *) echo "Unknown sorting order selected (${SORT_ORDER})." usage esac if [[ "${input_count}" -eq 2 ]] then case "${input_2}" in "--reverse") echo "Reverse ordering" SORT_REVERSE=1 ;; *) echo "Unknown option '${input_2}'" SORT_REVERSE=0 esac fi fi } connection_test() { local host_endpoint host_endpoint="${1}" if [[ $(ping -c 1 "${host_endpoint}" 2>&1 | grep -c "Name or service not known") -ne 0 ]] then echo -e "\nCan't connect to $host_endpoint. Please check your internet connection and try again.\n" exit 0 fi } function find_my_package_manager() { local i local managers_list local managers_priority_list local manager_priority local manager i=0 managers_list=() managers_priority_list=() for manager_str in ${!ARCH_MANAGERS[@]}; do OLDIFS=${IFS} IFS="," manager_array=(${manager_str}) IFS=${OLDIFS} manager_priority="${manager_array[0]}" manager="${manager_array[1]}" if [[ "${manager_priority}" -lt "${MANAGER_PRIORITY_LOWLIMIT}" ]] then echo "Minimum priority is $((${MANAGER_PRIORITY_LOWLIMIT} + 1)). You have a package which has lower priority value. Exiting." exit 1 fi if [[ $(type -P ${manager}) ]] then managers_list[$i]="${manager}" managers_priority_list[$i]=${manager_priority} let i++ fi done if [[ ${#managers_list[@]} -eq 0 ]] then echo "Not any valid package manager found. Exiting." exit 1 fi if [[ $(echo ${managers_priority_list[@]} | tr ' ' '\n' | uniq -d | wc -l) -ne 0 ]] then echo "Package managers with same priority found. Check internal manager list for duplicates. Exiting." exit 1 fi # Select package manager by priority. Highest is selected. i=0 while [[ "${i}" -le $((${#managers_list[@]} - 1)) ]] do if [[ ${managers_priority_list[i]} -gt ${priority_lowlimit} ]] then priority_lowlimit=${managers_priority_list[i]} SELECTED_MANAGER=${managers_list[i]} fi let i++ done OLDIFS=${IFS} IFS="|" pkg_command=(${ARCH_MANAGERS["$priority_lowlimit,$SELECTED_MANAGER"]}) IFS=${OLDIFS} command_refresh="${pkg_command[0]}" command_pkginfo_local="${pkg_command[1]}" command_pkginfo_local_short="${pkg_command[2]}" command_pkginfo_remote="${pkg_command[3]}" command_require_root="${pkg_command[4]}" if [[ "${command_require_root}" == "root" ]] then if [[ ! $(id -u) -eq 0 ]] then echo -e "\nThis command requires root privileges.\n" exit 0 fi fi } # TODO: We can't really depend on parsing output strings since they vary between Arch package managers package_version_parsed() { echo "${1}" | awk -F ' ' '{print $2}' | sed -r 's/[a-z]+.*//; s/[:_+-]/\./g; s/[^0-9]$//;' } package_version_check() { local system_version local repo_version local version_array_1 local version_array_2 local first_version_numbers local last_version_numbers local comparables local version_status_msg local check1 local check2 local s # Expected output syntax: "^ $" # TODO: We can't really depend on parsing output strings since they vary between Arch package managers system_version=$(${SELECTED_MANAGER} ${command_pkginfo_local_short} "${1}") repo_version=$(${SELECTED_MANAGER} ${command_pkginfo_remote} $1 | grep -E "^Version\s*:" | sed -r 's/.*(:\s*.*$)/\1/') version_array_1=$(package_version_parsed "${system_version}") version_array_2=$(package_version_parsed "${repo_version}") #Count of version elements (0 18 2 1 contains 4 numbers, for example) first_version_numbers=$(echo "${version_array_1}" | awk -F '.' '{print split($0, a)}') last_version_numbers=$(echo "${version_array_2}" | awk -F '.' '{print split($0, a)}') # Count of comparable version elements (maximum) # We compare this much of elements, not more if [[ "${last_version_numbers}" -lt "${first_version_numbers}" ]] then comparables="${last_version_numbers}" else comparables="${first_version_numbers}" fi # If all numbers are same, we don't analyze them more deeply. if [[ "${version_array_1}" == "${version_array_2}" ]] then version_status_msg="${green}Package is updated" else s=1 while [ ${s} -le ${comparables} ] do check1=$(echo -e "${version_array_1}" | awk -v var=$s -F '.' '{print $var}') check2=$(echo -e "${version_array_2}" | awk -v var=$s -F '.' '{print $var}') if [[ ${check2} -gt ${check1} ]] then # Repo number is greater version_status_msg="${yellow}Update available" break elif [[ ${check2} -lt ${check1} ]] then # System number is greater version_status_msg="${reset}Newer package installed" break fi let s++ done fi if [[ -z "${version_status_msg}" ]] then version_status_msg="${reset}Unknown" fi echo "${version_status_msg}" } exec_tool() { local i local package_count local risks_parsed_count local risks local description_column_max_chars local r_package_name local r_package_security_issues_count local r_package_security_issues_level local r_package_security_issues_level local r_package_description local risk_entries local sort_params local sort_column local package_alert_importance_status local package_alert_msg_color local package_alert_importance_output_status local package_security_issues_count local security_issues_package_summary local security_issues_total_count local security_msg_color i=1 description_column_max_chars=35 sort_params=() input_parser connection_test "${NETWORK_HOST_ENDPOINT}" find_my_package_manager echo "Security report date: $(date '+%d-%m-%Y, %X') (TZ: $(timedatectl status | grep "Time zone:" | awk '{print $3}'))" echo -e "\nSynchronizing package databases with ${SELECTED_MANAGER}\n" ${SELECTED_MANAGER} ${command_refresh} || exit if [[ ! $(type -P arch-audit) ]] then echo -e "\nCouldn't find Arch Linux security utility (arch-audit) in \$PATH. Please make sure it's installed.\n" else packages= package_count=0 risks_parsed_count=0 IFS=$'\n' for au in $(arch-audit); do package_name=$(echo "${au}" | awk -F ' ' '{print $1}') risk_level=$(echo "${au}" | grep -oE "Low|Medium|High|Critical") risks_count=$(echo "${au}" | grep -oP "(?<=by ).+(?=\. )" | sed 's/, /\n/g' | wc -l) risks[$package_count]="$package_name;$risk_level;$risks_count" packages="${packages}, ${package_name}" let package_count++ done echo -e "Analyzing ${#risks[*]} vulnerable packages. This takes a while...\n" echo -e "Vulnerable packages are:\n\n$(echo ${packages} | sed 's/^, //')\n" for risk_parsed in ${risks[@]}; do OLDIFS=${IFS} IFS=";" risk_parsed=(${risk_parsed}) IFS=${OLDIFS} # Package name r_package_name="${risk_parsed[0]}" echo -en "Analysing package ${i}/${#risks[*]} (${r_package_name})... \r" # Package security issues detected r_package_security_issues_count="${risk_parsed[2]}" # Package security issues overall level: Critical, High, Medium or Low 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') r_package_description=$(${SELECTED_MANAGER} "${command_pkginfo_local}" "${r_package_name}" | grep -i description | awk -F ": " '{print $2}') if [[ $(echo "${r_package_description}" | wc -m) -gt ${description_column_max_chars} ]] then r_package_description=$(printf "%s..." $(echo "${r_package_description}" | cut -c 1-${description_column_max_chars})) fi r_package_version_status=$(package_version_check "${r_package_name}") risk_entries[$risks_parsed_count]=$(printf "%s|%s|%s|%s|%s\n" \ "${r_package_name}" \ "${r_package_security_issues_count}" \ "${r_package_security_issues_level}" \ "${r_package_version_status}" \ "${r_package_description}" \ ) let risks_parsed_count++ let i++ done echo -e "\e[1m" printf "\n%-25s%-20s%-15s%-25s%s\n" "Package" "Security issues" "Risk level" "Version status" "Description" echo -e "\e[0m" case "${SORT_ORDER}" in name) sort_column="-k1" ;; issues) sort_column="-k2" sort_params+=("-n") ;; level) sort_column="-k3" ;; version) sort_column="-k4" ;; desc) sort_column="-k5" ;; #*) # echo "Unknown sorting order selected. Exiting." # exit 1 esac if [[ "${SORT_REVERSE}" -eq 1 ]] then sort_params+=("-r") fi i=0 IFS=$'\n' for line in $(echo "${risk_entries[*]}" | sort ${sort_params[*]} -t'|' ${sort_column}) do package_alert_importance_status=$(echo "${line}" | awk -F '|' '{print $3}') case "${package_alert_importance_status}" in 0) package_alert_msg_color="${red}" package_alert_importance_output_status="Critical" ;; 1) package_alert_msg_color="${orange}" package_alert_importance_output_status="High" ;; 2) package_alert_msg_color="${yellow}" package_alert_importance_output_status="Medium" ;; 3) package_alert_msg_color="${green}" package_alert_importance_output_status="Low" ;; esac package_security_issues_count=$(echo "${line}" | awk -F '|' '{print $2}') if [[ ${package_security_issues_count} -lt 5 ]] then security_msg_color="${green}" elif [[ ${package_security_issues_count} -ge 5 ]] && [[ ${package_security_issues_count} -lt 10 ]] then security_msg_color="${yellow}" elif [[ ${package_security_issues_count} -ge 10 ]] && [[ ${package_security_issues_count} -lt 20 ]] then security_msg_color="${orange}" else security_msg_color="${red}" fi security_issues_package_summary[$i]=${package_security_issues_count} echo "${line}" | awk -F '|' \ -v clr1="${package_alert_msg_color}" \ -v clr2="${security_msg_color}" \ -v rs="${reset}" \ -v var="${package_alert_importance_output_status}" \ '{printf "%-25s%s%-20s%s%-15s%-30s%s%s\n",$1,clr2,$2,clr1,var,$4,rs,$5}' let i++ done security_issues_total_count=$(echo $(printf "%d+" "${security_issues_package_summary[@]}")0 | bc) echo -e "\nTotal:" printf "%-25s%s\n\n" "${package_count}" "${security_issues_total_count}" printf "Check %s for more information.\n\n" "${NETWORK_HOST_ENDPOINT}" fi } exec_tool