diff --git a/tools/archrisks.sh b/tools/archrisks.sh old mode 100755 new mode 100644 index ede3b24..da1e2ca --- a/tools/archrisks.sh +++ b/tools/archrisks.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # archrisks - Get security risk severity & count of installed packages on Arch Linux -# Copyright (C) 2021 Pekka Helenius +# 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 @@ -53,319 +53,430 @@ ARCH_MANAGERS=( # [13,spinach]="-Syy|-Qi|-Q|-Si|no_root" # [14,trizen]="-Syy|-Qi|-Q|-Si|no_root" ) +SELECTED_MANAGER= +MANAGER_PRIORITY_LOWLIMIT=-1 -priority_lowlimit=-1 -default_order="level" -default_reverse=0 - -provider="security.archlinux.org" - - - +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} +[[ "${input_count}" -eq 1 ]] && input_1="${1}" +[[ "${input_count}" -eq 2 ]] && input_1="${1}"; input_2="${2}" -function helpCaller() { - echo -e " +usage() { + echo -e " Usage: $0 - -h|--help -1st arg: --sort= (optional) -2nd arg: --reverse (optional) + -h|--help + 1st arg: --sort= (optional) + 2nd arg: --reverse (optional) " - exit 0 + exit 0 } -function inputParser() { - - if [[ ${input_count} -gt 2 ]] || [[ ${input_1} == "-h" ]] || [[ ${input_1} == "--help" ]]; then - helpCaller - elif [[ ${input_count} -eq 0 ]]; then - sort_order=${default_order} - sort_reverse=${default_reverse} - else - 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})." - helpCaller - 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=${default_reverse} - esac - fi - +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 } -function internetTest() { - if [[ $(ping -c 1 $provider 2>&1 | grep -c "Name or service not known") -ne 0 ]]; then - echo -e "\nCan't connect to $provider. Please check your internet connection and try again.\n" - exit 0 - 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 findMyPackageManager() { +function find_my_package_manager() { - i=0 - for managerStr in ${!ARCH_MANAGERS[@]}; do + local i + local managers_list + local managers_priority_list + local manager_priority + local manager - manager_priority=$(echo ${managerStr} | awk -F ',' '{print $1}') - manager=$(echo ${managerStr} | awk -F ',' '{print $2}') + i=0 + managers_list=() + managers_priority_list=() - if [[ ${manager_priority} -lt ${priority_lowlimit} ]]; then - echo "Minimum priority is $((${priority_lowlimit} + 1)). You have a package which has lower priority value. Exiting." - exit 1 - fi + for manager_str in ${!ARCH_MANAGERS[@]}; do - if [[ $(echo $(which ${manager} &>/dev/null)$?) -eq 0 ]]; then - managers_list[$i]=${manager} - managers_priority_list[$i]=${manager_priority} - let i++ - fi - done + OLDIFS=${IFS} + IFS="," + manager_array=(${manager_str}) + IFS=${OLDIFS} + + manager_priority="${manager_array[0]}" + manager="${manager_array[1]}" - if [[ ${#managers_list[@]} -eq 0 ]]; then - echo "Not any valid package manager found. Exiting." - exit 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 [[ $(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 + 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 +} - # 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++ +# 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 - pkg_command=${ARCH_MANAGERS["$priority_lowlimit,$selected_manager"]} + echo "${version_status_msg}" +} - command_refresh=$(echo $pkg_command | awk -F '|' '{print $1}') - command_pkginfo_local=$(echo $pkg_command | awk -F '|' '{print $2}') - command_pkginfo_local_short=$(echo $pkg_command | awk -F '|' '{print $3}') - command_pkginfo_remote=$(echo $pkg_command | awk -F '|' '{print $4}') - command_require_root=$(echo $pkg_command | awk -F '|' '{print $5}') +exec_tool() { - if [[ ${command_require_root} == "root" ]]; then - if [[ ! $(id -u) -eq 0 ]]; then - echo -e "\nThis command requires root privileges.\n" - exit 0 - fi - fi + 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=() -function runTool() { - echo "Security report date: $(date '+%d-%m-%Y, %X') (TZ: $(timedatectl status | grep "Time zone:" | awk '{print $3}'))" + input_parser + connection_test "${NETWORK_HOST_ENDPOINT}" + find_my_package_manager - echo -e "\nSynchronizing package databases with ${selected_manager}\n" - ${selected_manager} ${command_refresh} || exit + 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" - if [[ ! $(which arch-audit | wc -l) -eq 1 ]]; then - echo -e "\nCouldn't find Arch Linux security utility (arch-audit) in \$PATH. Please make sure it's installed.\n" - else + ${SELECTED_MANAGER} ${command_refresh} || exit - count=0 - prs_count=0 - IFS=$'\n' + 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 - for i in $(arch-audit); do - package_name=$(echo "$i" | awk -F ' ' '{print $1}') - risk_level=$(echo "$i" | grep -oE "Low|Medium|High|Critical") - risks_count=$(echo "$i" | grep -oP "(?<=by ).+(?=\. )" | sed 's/, /\n/g' | wc -l) - #risks_count=$(echo "$i" | awk -F 'CVE' '{print NF-1}') + 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" - risks[$count]="$package_name $risk_level $risks_count" + for risk_parsed in ${risks[@]}; do - let count++ - done + OLDIFS=${IFS} + IFS=";" + risk_parsed=(${risk_parsed}) + IFS=${OLDIFS} - echo -e "\nAnalyzing ${#risks[*]} vulnerable packages. This takes a while...\n" + # Package name + r_package_name="${risk_parsed[0]}" - i=1 - for risk_parsed in $(echo "${risks[*]}"); do + echo -en "Analysing package ${i}/${#risks[*]} (${r_package_name})... \r" - echo -en "Analysing package ${i}/${#risks[*]}... \r" + # Package security issues detected + r_package_security_issues_count="${risk_parsed[2]}" - # Package in question - col1=$(echo "$risk_parsed" | awk -F ' ' '{print $1}') - - # Security issues detected - col2=$(echo "$risk_parsed" | awk -F ' ' '{print $3}') + # 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') - #Critical, High, Medium or Low risk - col3=$(echo "$risk_parsed" | awk -F ' ' '{print $2}' | sed 's/Critical/0/g; s/High/1/g; s/Medium/2/g; s/Low/3/g') - - col5=$(${selected_manager} ${command_pkginfo_local} $col1 | grep -i description | awk -F ": " '{print $2}') - maxchars=35 - - if [[ $(echo $col5 | wc -m) -gt $maxchars ]]; then - col5=$(echo "$(echo $col5 | cut -c 1-$maxchars)...") - fi - - versioncheck() { + r_package_description=$(${SELECTED_MANAGER} "${command_pkginfo_local}" "${r_package_name}" | grep -i description | awk -F ": " '{print $2}') - # TODO: We can't really depend on parsing output strings since they vary between Arch package managers - parsedver() { - echo $1 | awk -F ' ' '{print $2}' | sed -r 's/[a-z]+.*//; s/[:_+-]/\./g; s/[^0-9]$//;' - } - - # 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=$(parsedver $system_version) - version_array_2=$(parsedver $repo_version) - - #Count of version elements (0 18 2 1 contains 4 numbers, for example) - firstvernums=$(echo $version_array_1 | awk -F '.' '{print split($0, a)}') - lastvernums=$(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 [[ $lastvernums -lt $firstvernums ]]; then - comparables=$lastvernums - else - comparables=$firstvernums - fi - - # If all numbers are same, we don't analyze them more deeply. - if [[ $version_array_1 == $version_array_2 ]]; then - col4="${green}Package is updated" - else - - s=1 - while [ $s -le $comparables ]; do + 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 - 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}') + r_package_version_status=$(package_version_check "${r_package_name}") - if [[ $check2 -gt $check1 ]]; then - # Repo number is greater - col4="${yellow}Update available" - break + 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}" \ + ) - elif [[ $check2 -lt $check1 ]]; then - # System number is greater - col4="${reset}Newer package installed" - break - fi + let risks_parsed_count++ + let i++ + + done - let s++ - done - fi - } - - versioncheck $col1 - - risk_entries[$prs_count]=$(printf "%s|%s|%s|%s|%s\n" "$col1" "$col2" "$col3" "$col4" "$col5") - - let prs_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" - - sort_params=() - 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} == 1 ]]; then - sort_params+=("-r") - fi - - i=0 - for line in $(echo "${risk_entries[*]}" | sort ${sort_params[*]} -t'|' ${sort_column}); do - - if [[ $(echo "$line" | awk -F '|' '{print $3}') -eq 0 ]]; then - alert_color="${red}" - importance="Critical" - - elif [[ $(echo "$line" | awk -F '|' '{print $3}') -eq 1 ]]; then - alert_color="${orange}" - importance="High" - - elif [[ $(echo "$line" | awk -F '|' '{print $3}') -eq 2 ]]; then - alert_color="${yellow}" - importance="Medium" - - elif [[ $(echo "$line" | awk -F '|' '{print $3}') -eq 3 ]]; then - alert_color="${green}" - importance="Low" - fi - - sec_count=$(echo "$line" | awk -F '|' '{print $2}') - - if [[ $sec_count -lt 5 ]]; then - secclr="${green}" - elif [[ $sec_count -ge 5 ]] && [[ $sec_count -lt 10 ]]; then - secclr="${yellow}" - elif [[ $sec_count -ge 10 ]] && [[ $sec_count -lt 20 ]]; then - secclr="${orange}" - elif [[ $sec_count -ge 20 ]]; then - secclr="${red}" - fi - - secsum[$i]=$sec_count - - echo "$line" | awk -F '|' -v clr1="${alert_color}" -v clr2="${secclr}" -v rs="${reset}" -v var="${importance}" '{printf "%-25s%s%-20s%s%-15s%-30s%s%s\n",$1,clr2,$2,clr1,var,$4,rs,$5}' - - let i++ - done - - secsums_total=$(echo $(printf "%d+" ${secsum[@]})0 | bc) - - echo -e "\nTotal:" - printf "%-25s%s\n\n" "$count" "$secsums_total" - printf "Check %s for more information.\n\n" "${provider}" + 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 } -inputParser -findMyPackageManager -internetTest -runTool +exec_tool