Files
emba/modules/S20_shell_check.sh
2026-05-21 13:57:43 +02:00

230 lines
9.1 KiB
Bash
Executable File

#!/bin/bash -p
# EMBA - EMBEDDED LINUX ANALYZER
#
# Copyright 2020-2023 Siemens AG
# Copyright 2020-2026 Siemens Energy AG
#
# EMBA comes with ABSOLUTELY NO WARRANTY. This is free software, and you are
# welcome to redistribute it under the terms of the GNU General Public License.
# See LICENSE file for usage of this software.
#
# EMBA is licensed under GPLv3
# SPDX-License-Identifier: GPL-3.0-only
#
# Author(s): Michael Messner, Pascal Eckmann
# Description: Checks for bugs, stylistic errors, etc. in shell scripts, then it lists the found error types.
S20_shell_check() {
module_log_init "${FUNCNAME[0]}"
module_title "Check scripts with shellcheck and semgrep"
pre_module_reporter "${FUNCNAME[0]}"
export S20_SHELL_VULNS=0
export S20_SCRIPTS=0
local lSH_SCRIPTS_ARR=()
local lSH_SCRIPT=""
local lS20_VULN_TYPES_ARR=()
local lVTYPE=""
local lSEMGREP=1
local lNEG_LOG=0
local lS20_SEMGREP_ISSUES=0
local lWAIT_PIDS_S20_ARR=()
mapfile -t lSH_SCRIPTS_ARR < <(grep "shell script, ASCII text executable" "${P99_CSV_LOG}" | cut -d ';' -f2 | sort -u || true)
write_csv_log "Script path" "Shell issues detected" "common linux file" "shellcheck/semgrep"
if [[ ${SHELLCHECK} -eq 1 ]]; then
sub_module_title "Check scripts with shellcheck"
for lSH_SCRIPT in "${lSH_SCRIPTS_ARR[@]}"; do
((S20_SCRIPTS += 1))
if [[ "${THREADED}" -eq 1 ]]; then
s20_script_check "${lSH_SCRIPT/;*/}" &
local lTMP_PID="$!"
store_kill_pids "${lTMP_PID}"
lWAIT_PIDS_S20_ARR+=("${lTMP_PID}")
max_pids_protection "${MAX_MOD_THREADS}" lWAIT_PIDS_S20_ARR
continue
else
s20_script_check "${lSH_SCRIPT/;*/}"
fi
done
[[ "${THREADED}" -eq 1 ]] && wait_for_pid "${lWAIT_PIDS_S20_ARR[@]}"
if [[ -f "${TMP_DIR}"/S20_VULNS.tmp ]]; then
S20_SHELL_VULNS=$(awk '{sum += $1 } END { print sum }' "${TMP_DIR}"/S20_VULNS.tmp)
rm "${TMP_DIR}"/S20_VULNS.tmp
fi
[[ "${S20_SHELL_VULNS}" -gt 0 ]] && lNEG_LOG=1
print_ln
if [[ "${S20_SHELL_VULNS}" -gt 0 ]]; then
sub_module_title "Summary of shell issues (shellcheck)"
print_output "[+] Found ${ORANGE}${S20_SHELL_VULNS} issues${GREEN} in ${ORANGE}${S20_SCRIPTS}${GREEN} shell scripts (shellcheck mode)${NC}\\n"
fi
write_log ""
write_log "[*] Statistics:${S20_SHELL_VULNS}:${S20_SCRIPTS}"
mapfile -t lS20_VULN_TYPES_ARR < <(grep "\^--\ SC[0-9]" "${LOG_PATH_MODULE}"/shellchecker_* 2>/dev/null | cut -d: -f2- | sed -e 's/\ \+\^--\ //g' | sed -e 's/\^--\ //g' | sort -u -t: -k1,1 | sed -e 's/ \\n//' || true)
for lVTYPE in "${lS20_VULN_TYPES_ARR[@]}"; do
print_output "$(indent "${NC}[${GREEN}+${NC}]${GREEN} ${lVTYPE}${NC}")"
done
else
print_output "[-] Shellchecker is disabled ... no tests performed"
fi
if [[ ${lSEMGREP} -eq 1 ]]; then
sub_module_title "Check shell scripts with semgrep"
local lS20_SEMGREP_SCRIPTS=0
local lS20_SEMGREP_VULNS=0
local lSHELL_LOG="${LOG_PATH_MODULE}"/semgrep.log
semgrep --disable-version-check --config "${EXT_DIR}"/semgrep-rules/bash "${LOG_DIR}"/firmware/ >"${lSHELL_LOG}" 2>&1 || true
if [[ -s "${lSHELL_LOG}" ]]; then
lS20_SEMGREP_ISSUES=$(grep "\ findings\." "${lSHELL_LOG}" | cut -d: -f2 | awk '{print $1}' || true)
lS20_SEMGREP_VULNS=$(grep -c "semgrep-rules.bash.lang.security" "${lSHELL_LOG}" || true)
lS20_SEMGREP_SCRIPTS=$(grep "\ findings\." "${lSHELL_LOG}" | awk '{print $5}' || true)
if [[ "${lS20_SEMGREP_VULNS}" -gt 0 ]]; then
print_output "[+] Found ${ORANGE}${lS20_SEMGREP_ISSUES} issues${GREEN} (${ORANGE}${lS20_SEMGREP_VULNS} vulnerabilities${GREEN}) in ${ORANGE}${lS20_SEMGREP_SCRIPTS}${GREEN} shell scripts (semgrep mode)${NC}" "" "${lSHELL_LOG}"
elif [[ "${lS20_SEMGREP_ISSUES}" -gt 0 ]]; then
print_output "[+] Found ${ORANGE}${lS20_SEMGREP_ISSUES} issues${GREEN} in ${ORANGE}${lS20_SEMGREP_SCRIPTS}${GREEN} shell scripts${NC}" "" "${lSHELL_LOG}"
fi
# highlight security findings in semgrep log:
sed -i -r "s/.*external\.semgrep-rules\.bash\.lang\.security.*/\x1b[32m&\x1b[0m/" "${lSHELL_LOG}"
else
print_output "[-] No shell issues found with semgrep"
fi
[[ "${lS20_SEMGREP_ISSUES}" -gt 0 ]] && lNEG_LOG=1
write_log ""
write_log "[*] Statistics1:${lS20_SEMGREP_ISSUES}:${lS20_SEMGREP_SCRIPTS}"
else
print_output "[-] Semgrepper is disabled ... no tests performed"
fi
s20_eval_script_check "${lSH_SCRIPTS_ARR[@]}"
module_end_log "${FUNCNAME[0]}" "${lNEG_LOG}"
}
s20_eval_script_check() {
local lSH_SCRIPTS_ARR=("${@}")
local lSH_SCRIPT=""
# we rate eval usages higher compared to other shellcheck issues
local lGPT_PRIO_=4
local lAI_ANCHOR_=""
local lEVAL_RESULTS=0
local lSH_SCRIPT_NAME=""
sub_module_title "Summary of shell eval usages"
for lSH_SCRIPT in "${lSH_SCRIPTS_ARR[@]}"; do
# print_output "[*] Testing ${ORANGE}${lSH_SCRIPT}${NC} for eval usage" "no_log"
if grep "eval " "${lSH_SCRIPT/;*/}" | grep -q -v "^#.*"; then
lEVAL_RESULTS=1
lSH_SCRIPT_NAME="$(basename "${lSH_SCRIPT/;*/}")"
local lSHELL_LOG="${LOG_PATH_MODULE}"/sh_eval_sources/"${lSH_SCRIPT_NAME}".log
! [[ -d "${LOG_PATH_MODULE}"/sh_eval_sources/ ]] && mkdir "${LOG_PATH_MODULE}"/sh_eval_sources/
[[ -f "${lSH_SCRIPT/;*/}" ]] && cp "${lSH_SCRIPT/;*/}" "${lSHELL_LOG}"
sed -i -r "s/.*eval\ .*/\x1b[32m&\x1b[0m/" "${lSHELL_LOG}"
print_output "[+] Found ${ORANGE}eval${GREEN} usage in ${ORANGE}${lSH_SCRIPT_NAME}${NC}" "" "${lSHELL_LOG}"
if [[ "${AI_OPTION}" -gt 0 ]]; then
lAI_ANCHOR_="$(openssl rand -hex 8)"
if [[ -f "${BASE_LINUX_FILES}" ]]; then
# if we have the base linux config file we are checking it:
if ! grep -E -q "^${lSH_SCRIPT_NAME}$" "${BASE_LINUX_FILES}" 2>/dev/null; then
lGPT_PRIO_=$((lGPT_PRIO_ + 1))
fi
fi
# "${GPT_INPUT_FILE_}" "${lAI_ANCHOR_}" "GPT-Prio-$lGPT_PRIO_" "${GPT_QUESTION_}" "${GPT_OUTPUT_FILE_}" "cost=$GPT_TOKENS_" "${GPT_RESPONSE_}"
write_csv_AI_tmp "$(cut_path "${lSH_SCRIPT/;*/}")" "${lAI_ANCHOR_}" "${lGPT_PRIO_}" "${GPT_QUESTION}" "${lSHELL_LOG}" "" ""
# add ChatGPT link
printf '%s\n\n' "" >>"${lSHELL_LOG}"
write_anchor_AI "${lAI_ANCHOR_}" "${lSHELL_LOG}"
fi
fi
done
if [[ "${lEVAL_RESULTS}" -eq 0 ]]; then
print_output "[-] No eval usage found in shell scripts"
fi
}
s20_script_check() {
local lSH_SCRIPT_="${1:-}"
local lSH_NAME=""
local lSHELL_LOG=""
local lVULNS=""
lSH_NAME=$(basename "${lSH_SCRIPT_}" 2>/dev/null | sed -e 's/:/_/g')
lSHELL_LOG="${LOG_PATH_MODULE}/shellchecker_${lSH_NAME}.txt"
shellcheck -C "${lSH_SCRIPT_}" >"${lSHELL_LOG}" 2>/dev/null || true
lVULNS=$(grep -c "\\^-- SC" "${lSHELL_LOG}" 2>/dev/null || true)
s20_reporter "${lVULNS}" "${lSH_SCRIPT_}" "${lSHELL_LOG}"
}
s20_reporter() {
local lVULNS="${1:-}"
local lSH_SCRIPT_="${2:-}"
local lSHELL_LOG="${3:-}"
local lGPT_PRIO_=2
local lAI_ANCHOR_=""
local lSH_NAME=""
lSH_NAME=$(basename "${lSH_SCRIPT_}" 2>/dev/null | sed -e 's/:/_/g')
local lS20_SOURCE_DIR="${LOG_PATH_MODULE}/sh_sources"
if [[ "${lVULNS}" -ne 0 ]]; then
[[ ! -d "${lS20_SOURCE_DIR}" ]] && mkdir -p "${lS20_SOURCE_DIR}"
# safe the sources and link it in the lSHELL_LOG
write_log "" "${lSHELL_LOG}"
write_log "[*] Source file ${ORANGE}${lSH_NAME}${NC}" "${lSHELL_LOG}"
copy_and_link_file "${lSH_SCRIPT_}" "${lS20_SOURCE_DIR}/${lSH_NAME}.log" "${lSHELL_LOG}"
# check if this is common linux file:
local lCOMMON_FILES_FOUND=""
local lCFF=""
if [[ -f "${BASE_LINUX_FILES}" ]]; then
lCOMMON_FILES_FOUND="(${RED}common linux file: no${GREEN})"
lCFF="no"
if grep -q "^${lSH_NAME}\$" "${BASE_LINUX_FILES}" 2>/dev/null; then
lCOMMON_FILES_FOUND="(${CYAN}common linux file: yes${GREEN})"
lCFF="yes"
fi
else
lCOMMON_FILES_FOUND=""
fi
if [[ "${lVULNS}" -gt 20 ]]; then
print_output "[+] Found ${RED}${lVULNS} issues${GREEN} in script ${lCOMMON_FILES_FOUND}:${NC} $(print_path "${lSH_SCRIPT_}")" "" "${lSHELL_LOG}"
lGPT_PRIO_=$((lGPT_PRIO_ + 1))
else
print_output "[+] Found ${ORANGE}${lVULNS} issues${GREEN} in script ${lCOMMON_FILES_FOUND}:${NC} $(print_path "${lSH_SCRIPT_}")" "" "${lSHELL_LOG}"
fi
# scripts with eval usage are higher rated
if grep -q "eval" "${lS20_SOURCE_DIR}/${lSH_NAME}.log"; then
lGPT_PRIO_=$((lGPT_PRIO_ + 2))
fi
write_csv_log "$(print_path "${lSH_SCRIPT_}")" "${lVULNS}" "${lCFF}" "NA"
if [[ "${AI_OPTION}" -gt 0 ]]; then
lAI_ANCHOR_="$(openssl rand -hex 8)"
# "${GPT_INPUT_FILE_}" "${lAI_ANCHOR_}" "GPT-Prio-$lGPT_PRIO_" "${GPT_QUESTION_}" "${GPT_OUTPUT_FILE_}" "cost=$GPT_TOKENS_" "${GPT_RESPONSE_}"
write_csv_AI_tmp "${lS20_SOURCE_DIR}/${lSH_NAME}.log" "${lAI_ANCHOR_}" "${lGPT_PRIO_}" "${GPT_QUESTION}" "${lSHELL_LOG}" "" ""
# add AI link
printf '%s\n\n' "" >>"${lSHELL_LOG}"
write_anchor_AI "${lAI_ANCHOR_}" "${lSHELL_LOG}"
printf '%s\n\n' "" >>"${lS20_SOURCE_DIR}/${lSH_NAME}.log"
write_anchor_AI "${lAI_ANCHOR_}" "${lS20_SOURCE_DIR}/${lSH_NAME}.log"
fi
echo "${lVULNS}" >>"${TMP_DIR}"/S20_VULNS.tmp
fi
}