#!/usr/bin/env bash # enhanced_code_scan.sh # Усиленный офлайн-сканер "почти как антивирус" для кода/файлов/папок/архивов (macOS/Linux) set -euo pipefail VERSION="2.0.1" # ======= Конфигурация ======= REQUIRED_CMDS=( "yara" "clamscan" "semgrep" "gitleaks" "syft" "osv-scanner" "7z" "jq" "file" "python3" ) OPTIONAL_CMDS=( "trufflehog" "shellcheck" ) YARA_RULESET_NAME="suspicious_rules.yar" DEFAULT_OUT="scan_results" SCAN_PASSWORD="${SCAN_PASSWORD:-}" # пароль к архивам (опционально). Можно передать аргументом --password MAX_TARGET_BYTES="${MAX_TARGET_BYTES:-200MB}" # лимит размера для Semgrep FRESHCLAM_AUTO="${FRESHCLAM_AUTO:-1}" # 1 = попытаться обновить базы ClamAV SHELL_BEHAVIOR_JSON="shell_behavior_findings.json" # отчёт поведенческого сканера export SEMGREP_SEND_METRICS=off # делаем Semgrep тише export PYTHONWARNINGS=ignore # приглушим ворнинги от семгреповского Python # ============================ # ---- утилиты вывода ---- log(){ printf "[*] %s\n" "$*"; } ok(){ printf "✅ %s\n" "$*"; } warn(){ printf "⚠️ %s\n" "$*" >&2; } err(){ printf "❌ %s\n" "$*" >&2; } usage(){ cat < [path2 ...] $(basename "$0") --install # установить зависимости (macOS: brew; Linux: apt + install-скрипты) $(basename "$0") --help Опции: -o, --out DIR Директория для отчётов (по умолчанию: ${DEFAULT_OUT}) --password PWD Пароль для зашифрованных архивов (альт.: переменная окружения SCAN_PASSWORD) --no-sbom Не строить SBOM и не сканировать уязвимости зависимостей --quick Быстрый режим (пропустить Semgrep, SBOM/OSV и shellcheck) --no-freshclam Не обновлять базы ClamAV перед сканом --max-bytes SIZE Лимит размера для Semgrep (по умолчанию ${MAX_TARGET_BYTES}) Примеры: $(basename "$0") ./project SCAN_PASSWORD=secret $(basename "$0") --out out ./archive.zip $(basename "$0") --quick /path/to/code1 /path/to/code2 EOF } OS="unknown" PKG="unknown" detect_os(){ case "$(uname -s)" in Darwin) OS="mac";; Linux) OS="linux";; *) OS="unknown";; esac if command -v brew >/dev/null 2>&1; then PKG="brew" elif command -v apt-get >/dev/null 2>&1; then PKG="apt" else PKG="unknown" fi } # Печать команд установки (и запуск при --install) print_install_instructions(){ detect_os echo warn "Не все зависимости установлены. Можно:" echo " 1) Запустить авто-установку: $(basename "$0") --install" echo " 2) Или поставить вручную командами ниже:" echo if [[ "$OS" == "mac" && "$PKG" == "brew" ]]; then cat <<'MAC' # macOS (Homebrew) brew update brew install python yara clamav semgrep gitleaks p7zip jq brew install anchore/syft/syft brew install osv-scanner brew install shellcheck # опционально, но желательно # (опционально) trufflehog: brew install trufflesecurity/trufflehog/trufflehog # После установки ClamAV один раз обновите базы: sudo freshclam MAC elif [[ "$OS" == "linux" && "$PKG" == "apt" ]]; then cat <<'LINUX' # Debian/Ubuntu sudo apt-get update sudo apt-get install -y python3 python3-venv python3-pip yara clamav jq p7zip-full unzip tar file shellcheck # semgrep и (опц.) trufflehog через pipx: python3 -m pip install --user pipx ~/.local/bin/pipx ensurepath ~/.local/bin/pipx install semgrep ~/.local/bin/pipx install trufflehog # опционально # syft: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin # osv-scanner: curl -sSfL https://raw.githubusercontent.com/google/osv-scanner/main/install.sh | sudo sh -s -- -b /usr/local/bin # gitleaks: curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh | sudo bash -s -- -b /usr/local/bin # Обновить базы ClamAV: sudo freshclam LINUX else cat <<'GEN' # Установщик пакетов не определён. Поставьте утилиты вручную: # обязательные: python3, yara, clamscan (clamav), semgrep, gitleaks, syft, osv-scanner, 7z, jq, file # опционально: trufflehog, shellcheck GEN fi } do_install(){ detect_os if [[ "$OS" == "mac" && "$PKG" == "brew" ]]; then log "Устанавливаю зависимости через Homebrew..." brew update brew install python yara clamav semgrep gitleaks p7zip jq || true brew install anchore/syft/syft || true brew install osv-scanner || true command -v shellcheck >/dev/null 2>&1 || brew install shellcheck || true command -v trufflehog >/dev/null 2>&1 || brew install trufflesecurity/trufflehog/trufflehog || true if [[ "${FRESHCLAM_AUTO}" == "1" ]]; then log "Обновляю базы ClamAV (sudo)..." sudo freshclam || warn "freshclam не удалось (можно запустить вручную)" fi ok "Установка завершена." elif [[ "$OS" == "linux" && "$PKG" == "apt" ]]; then log "Устанавливаю зависимости через apt..." sudo apt-get update sudo apt-get install -y python3 python3-venv python3-pip yara clamav jq p7zip-full unzip tar file shellcheck || true if ! command -v pipx >/dev/null 2>&1; then python3 -m pip install --user pipx ~/.local/bin/pipx ensurepath || true export PATH="$HOME/.local/bin:$PATH" fi command -v semgrep >/dev/null 2>&1 || pipx install semgrep || true command -v trufflehog >/dev/null 2>&1 || pipx install trufflehog || true command -v syft >/dev/null 2>&1 || curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin || true command -v osv-scanner >/dev/null 2>&1 || curl -sSfL https://raw.githubusercontent.com/google/osv-scanner/main/install.sh | sudo sh -s -- -b /usr/local/bin || true command -v gitleaks >/dev/null 2>&1 || curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh | sudo bash -s -- -b /usr/local/bin || true if [[ "${FRESHCLAM_AUTO}" == "1" ]]; then log "Обновляю базы ClamAV (sudo)..." sudo freshclam || warn "freshclam не удалось (можно запустить вручную)" fi ok "Установка завершена." else err "Авто-установка недоступна на этой системе." print_install_instructions exit 2 fi } have_cmd(){ command -v "$1" >/dev/null 2>&1; } check_deps(){ local missing=() for c in "${REQUIRED_CMDS[@]}"; do if ! have_cmd "$c"; then missing+=("$c"); fi done if ((${#missing[@]})); then warn "Отсутствуют зависимости: ${missing[*]}" print_install_instructions return 1 fi return 0 } create_yara_rules(){ local out_dir="$1" cat > "${out_dir}/${YARA_RULESET_NAME}" <<'YAR' rule Suspicious_Mining_Strings { meta: desc = "Частые строки майнеров/пулов" strings: $a = "stratum+tcp" nocase $b = "xmrig" nocase $c = "minerd" nocase $d = "cryptonight" nocase $e = /wallet[_-]?address/i condition: any of them } rule Suspicious_Obfuscation_Generic { meta: desc = "Обфускация/выполнение кода в разных языках" strings: $a = /base64_decode\s*\(/ nocase $b = /atob\s*\(/ nocase $c = /-EncodedCommand/ nocase $d = /eval\s*\(/ nocase $e = /new Function\s*\(/ nocase $f = /__import__\(['"]base64/ nocase condition: 2 of them } rule Suspicious_Recon_Networking { meta: desc = "Сети/бэконнекты/туннели/скам-домены" strings: $a = /\/bin\/sh\s+-c/ nocase $b = /\/dev\/tcp\/[0-9\.]+\/[0-9]+/ nocase $c = "ngrok.io" nocase $d = "serveo.net" nocase $e = /curl\s+.{0,200}\|\s*.{0,10}sh/ nocase $f = "pastebin.com" nocase $g = "bit.ly" nocase $h = "t.me/" nocase $i = "discord.gg" nocase condition: any of them } YAR } # ====== Поведенческий сканер shell (встраиваемый Python) ====== write_shell_behavior_scanner(){ local py="$1" cat > "$py" <<'PY' #!/usr/bin/env python3 import sys, re, json, os, io def clean_line(line:str) -> str: s = [] in_s = in_d = False i = 0 while i < len(line): ch = line[i] if ch == '\\' and i+1 < len(line): if in_s or in_d: s.append(' ') i += 2 continue if not in_s and not in_d and ch == '#': break if ch == "'" and not in_d: in_s = not in_s; s.append(' ') elif ch == '"' and not in_s: in_d = not in_d; s.append(' ') else: s.append(' ' if (in_s or in_d) else ch) i += 1 return ''.join(s) RULES = [ ("CRITICAL", r'\|\s*(?:sh|bash|zsh)\b'), ("CRITICAL", r'\b(?:bash|sh)\s+-c\s'), ("CRITICAL", r'>\s*&\s*/dev/tcp/\S+|/dev/tcp/\S+\s'), ("CRITICAL", r'\bnc\b.*\s-(?:e|c)\b|\bnetcat\b.*\s-(?:e|c)\b'), ("CRITICAL", r'\bmkfifo\b.*\bnc\b.*\bsh\b'), ("CRITICAL", r'\bcurl\b.*\|\s*sh\b|\bwget\b.*\|\s*(?:sh|bash)\b'), ("CRITICAL", r'\btftp\b.*\bget\b'), ("CRITICAL", r'\bchmod\s+4(?:7|6)\d\d\b'), ("CRITICAL", r'\becho\b.*>>\s*/etc/sudoers\b|\bvisudo\b.*-f\b'), ("CRITICAL", r'/etc/shadow|/etc/security/passwd'), ("HIGH", r'\b(service|systemctl)\s+stop\s+auditd\b|\baudctl\b.*-D\b'), ("HIGH", r'\bhistory\s+-c\b|\bHISTFILESIZE=0\b|\bHISTSIZE=0\b'), ("HIGH", r'>\s*(?:~\/)?\.(?:bash|zsh)_history\b|\btruncate\b.*history'), ("HIGH", r'\bcron(?:tab)?\b.*\b-e\b|\bcrontab\b.*\|\s*crontab\b'), ("HIGH", r'\baws\s+iam\s+(?:add-user-to-group|attach-user-policy)\b'), ("HIGH", r'\b(chown|setfacl)\b.*\sroot\b'), ("MEDIUM", r'\bnmap\b|\bmasscan\b'), ("MEDIUM", r'\bwget\b|\bcurl\b(?!.*\|\s*(?:sh|bash)\b)'), ("MEDIUM", r'\bnc\b\s+-l'), ("MEDIUM", r'\bhttp_proxy\s*='), ("MEDIUM", r'\baws\s+sts\s+get-caller-identity\b'), ("MEDIUM", r'\b(?:scp|rsync)\s+\S+@'), ("LOW", r'\b(?:whoami|id|uname|lsb_release|hostnamectl)\b'), ("LOW", r'\b(find|grep|ls)\b'), ] SHELL_LIKE = ('.sh', '.bash', '.zsh', '.ksh', '.dash') def looks_like_shell(path:str, first_line:str) -> bool: if path.endswith(SHELL_LIKE): return True if first_line.startswith('#!') and any(s in first_line for s in ('/sh','/bash','/zsh','/dash','/ksh')): return True return False def scan_file(path:str): try: with io.open(path, 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines() except Exception: return [] first = lines[0] if lines else '' if not looks_like_shell(path, first): return [] findings = [] for idx, raw in enumerate(lines, 1): line = clean_line(raw) if not line.strip(): continue for sev, pat in RULES: if re.search(pat, line): findings.append({ "path": path, "line": idx, "severity": sev, "pattern": pat, "snippet": raw.rstrip()[:300] }) return findings def main(): files = sys.argv[1:] result = [] for p in files: if os.path.isdir(p): for root, _, fnames in os.walk(p): for fn in fnames: fp = os.path.join(root, fn) result.extend(scan_file(fp)) else: result.extend(scan_file(p)) print(json.dumps(result, ensure_ascii=False)) if __name__ == "__main__": main() PY chmod +x "$py" } tmpdirs=() mkdtemp_(){ d=$(mktemp -d); tmpdirs+=("$d"); echo "$d"; } cleanup(){ for d in "${tmpdirs[@]:-}"; do [[ -d "$d" ]] && rm -rf "$d" || true; done; } trap cleanup EXIT is_archive(){ local f="$1" case "${f,,}" in *.zip|*.7z|*.rar|*.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tbz|*.tar.xz|*.txz) return 0 ;; *) return 1 ;; esac } list_archive_with_7z(){ 7z l -slt -- "$1" 2>/dev/null || return 1 } extract_archive(){ local archive="$1" dest="$2" pw="$3" mkdir -p "$dest" if [[ -n "$pw" ]]; then 7z x -p"$pw" -aos -o"$dest" -- "$archive" >/dev/null else 7z x -aos -o"$dest" -- "$archive" >/dev/null fi } scan_shell_behavior(){ local target="$1" out_dir="$2" quick="$3" if [[ "$quick" == "1" ]]; then return 0 fi local pyscan="${out_dir}/_sh_behavior_scan.py" write_shell_behavior_scanner "$pyscan" local out_json="${out_dir}/${SHELL_BEHAVIOR_JSON%.*}_$(basename "$target").json" python3 "$pyscan" "$target" > "$out_json" || echo "[]" > "$out_json" # Если есть shellcheck — добавим отчёт (совместимо с macOS find, без -maxdepth) if have_cmd shellcheck; then find "$target" -type f -print0 2>/dev/null | \ while IFS= read -r -d '' f; do head -n1 "$f" | grep -qE '^#!.*(sh|bash|zsh|dash|ksh)' || [[ "$f" =~ \.sh$|\.bash$|\.zsh$|\.ksh$|\.dash$ ]] || continue shellcheck -f json "$f" 2>/dev/null || true done | jq -s 'flatten' > "${out_dir}/shellcheck_$(basename "$target").json" || echo "[]" > "${out_dir}/shellcheck_$(basename "$target").json" fi } scan_target(){ local target="$1" local out_dir="$2" local quick="${3}" local nosbom="${4}" local yara_rules="${out_dir}/${YARA_RULESET_NAME}" log "Анализ: $target" # 1) ClamAV if have_cmd clamscan; then clamscan -ri -- "$target" | tee "${out_dir}/clamav_$(basename "$target").txt" >/dev/null || true fi # 2) YARA if have_cmd yara; then yara -r "$yara_rules" "$target" | tee "${out_dir}/yara_$(basename "$target").txt" >/dev/null || true fi # 3) Поведенческий сканер shell scan_shell_behavior "$target" "$out_dir" "$quick" if [[ "$quick" == "1" ]]; then warn "Быстрый режим: пропускаем Semgrep и SBOM/OSV." else # 4) Semgrep (универсально для новых/старых версий) if have_cmd semgrep; then if semgrep scan --help >/dev/null 2>&1; then semgrep scan --config p/security-audit --error --json --metrics=off \ --timeout 180 --max-target-bytes "$MAX_TARGET_BYTES" \ -- "$target" > "${out_dir}/semgrep_$(basename "$target").json" || true else semgrep ci --config p/security-audit --json --metrics=off \ --timeout 180 --max-target-bytes "$MAX_TARGET_BYTES" \ -- "$target" > "${out_dir}/semgrep_$(basename "$target").json" || true fi fi # 5) SBOM + OSV (если не отключено) if [[ "$nosbom" != "1" ]] && have_cmd syft && have_cmd osv-scanner; then syft "dir:${target}" -o cyclonedx-json > "${out_dir}/sbom_$(basename "$target").json" || true osv-scanner --sbom="${out_dir}/sbom_$(basename "$target").json" > "${out_dir}/osv_$(basename "$target").txt" || true fi fi # 6) Секреты if have_cmd gitleaks; then gitleaks detect -s "$target" --no-git -r "${out_dir}/gitleaks_$(basename "$target").json" || true fi if have_cmd trufflehog; then trufflehog filesystem "$target" --json > "${out_dir}/trufflehog_$(basename "$target").json" || true fi # 7) Общие эвристики { echo "# base64-подобные длинные строки:" grep -RInaE '([A-Za-z0-9+/]{40,}={0,2})' "$target" || true echo echo "# потенциальные команды исполнения/обфускации:" grep -RInaE '(^|[^[:alnum:]_])eval\s*\(|atob\s*\(|base64_decode\s*\(|-EncodedCommand|/bin/sh -c' "$target" || true echo echo "# майнинг/кошельки/пулы:" grep -RInaE 'stratum\+tcp|xmrig|wallet|monero|pool\.' "$target" || true } > "${out_dir}/heuristics_$(basename "$target").txt" # 8) Бинарники if have_cmd file; then if find "$target" -type f -perm -111 -print0 2>/dev/null | xargs -0 file -b 2>/dev/null | grep -Eq 'ELF|Mach-O'; then echo "Executable binaries present (ELF/Mach-O). Verify origin." >> "${out_dir}/heuristics_$(basename "$target").txt" fi fi ok "Готово: $target" } summarize_findings(){ local out_dir="$1" local summary="${out_dir}/SUMMARY.txt" : > "$summary" echo "=== Итоги сканирования ===" >> "$summary" echo "Время: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$summary" echo >> "$summary" # ClamAV if ls "${out_dir}"/clamav_*.txt >/dev/null 2>&1; then echo "[ClamAV]" >> "$summary" grep -Hn "FOUND" "${out_dir}"/clamav_*.txt || echo "нет срабатываний" >> "$summary" echo >> "$summary" fi # YARA if ls "${out_dir}"/yara_*.txt >/dev/null 2>&1; then echo "[YARA]" >> "$summary" if grep -q . "${out_dir}"/yara_*.txt; then cat "${out_dir}"/yara_*.txt >> "$summary" else echo "нет срабатываний" >> "$summary" fi echo >> "$summary" fi # Поведение shell if ls "${out_dir}"/shell_behavior_findings_*.json >/dev/null 2>&1; then echo "[Shell behavior]" >> "$summary" if command -v jq >/dev/null 2>&1; then local total total=$(jq -s '[.[]] | flatten | length' "${out_dir}"/shell_behavior_findings_*.json 2>/dev/null || echo 0) echo "Найдено индикаторов: ${total}" >> "$summary" jq -s '[.[]] | flatten | sort_by( ((.severity=="CRITICAL")*3) + ((.severity=="HIGH")*2) + ((.severity=="MEDIUM")*1) ) | reverse | .[:20]' "${out_dir}"/shell_behavior_findings_*.json 2>/dev/null >> "$summary" || true else echo "jq не установлен — см. JSON файлы shell_behavior_findings_*.json" >> "$summary" fi echo >> "$summary" fi # Semgrep if ls "${out_dir}"/semgrep_*.json >/dev/null 2>&1; then echo "[Semgrep] (подозрительные паттерны/уязвимости)" >> "$summary" if command -v jq >/dev/null 2>&1; then for f in "${out_dir}"/semgrep_*.json; do cnt=$(jq '[.results[]?] | length' "$f" 2>/dev/null || echo 0) echo "$(basename "$f"): $cnt находок" >> "$summary" done else echo "jq не установлен — пропускаю агрегацию" >> "$summary" fi echo >> "$summary" fi # Secrets if ls "${out_dir}"/gitleaks_*.json >/dev/null 2>&1; then echo "[Secrets: Gitleaks]" >> "$summary" if command -v jq >/dev/null 2>&1; then for f in "${out_dir}"/gitleaks_*.json; do cnt=$(jq '.leaks | length' "$f" 2>/dev/null || echo 0) echo "$(basename "$f"): $cnt потенциальных секретов" >> "$summary" done else echo "jq не установлен — пропускаю агрегацию" >> "$summary" fi echo >> "$summary" fi if ls "${out_dir}"/trufflehog_*.json >/dev/null 2>&1; then echo "[Secrets: TruffleHog]" >> "$summary" for f in "${out_dir}"/trufflehog_*.json; do echo "$(basename "$f"): $(grep -c '"DetectorName"' "$f" || echo 0) потенциальных секретов" >> "$summary" done echo >> "$summary" fi # OSV if ls "${out_dir}"/osv_*.txt >/dev/null 2>&1; then echo "[OSV/Уязвимости зависимостей]" >> "$summary" for f in "${out_dir}"/osv_*.txt; do echo "— $(basename "$f")" >> "$summary" grep -E "ID: |Severity: |Package Name:" "$f" | sed 's/^/ /' >> "$summary" || true done echo >> "$summary" fi # ShellCheck if ls "${out_dir}"/shellcheck_*.json >/dev/null 2>&1; then echo "[ShellCheck]" >> "$summary" if command -v jq >/dev/null 2>&1; then for f in "${out_dir}"/shellcheck_*.json; do cnt=$(jq 'length' "$f" 2>/dev/null || echo 0) echo "$(basename "$f"): $cnt замечаний" >> "$summary" done else echo "jq не установлен — см. JSON shellcheck_*.json" >> "$summary" fi echo >> "$summary" fi # Эвристики if ls "${out_dir}"/heuristics_*.txt >/dev/null 2>&1; then echo "[Эвристики]" >> "$summary" for f in "${out_dir}"/heuristics_*.txt; do echo "— $(basename "$f")" >> "$summary" head -n 20 "$f" | sed 's/^/ /' >> "$summary" done echo >> "$summary" fi # Зашифрованные/непрочитанные if [[ -s "${out_dir}/ENCRYPTED_OR_UNREADABLE.txt" ]]; then echo "[Зашифрованные/непрочитанные объекты]" >> "$summary" cat "${out_dir}/ENCRYPTED_OR_UNREADABLE.txt" >> "$summary" echo >> "$summary" fi ok "Итоговый отчёт: ${summary}" } note_encrypted(){ local out_dir="$1" path="$2" reason="$3" echo "${path} :: ${reason}" >> "${out_dir}/ENCRYPTED_OR_UNREADABLE.txt" } scan_archive_or_item(){ local path="$1" out_dir="$2" quick="$3" nosbom="$4" if is_archive "$path"; then log "Обнаружен архив: $path — пытаюсь прочитать заголовки" local lst if lst=$(list_archive_with_7z "$path"); then if grep -q "^Encrypted = \+" <<<"$lst"; then warn "Архив помечен как зашифрованный" if [[ -z "$SCAN_PASSWORD" ]]; then warn "Пароль не передан. Добавьте --password или SCAN_PASSWORD=..." note_encrypted "$out_dir" "$path" "encrypted archive (no password)" return 0 fi fi local exdir; exdir=$(mkdtemp_) log "Распаковываю в: $exdir" if ! extract_archive "$path" "$exdir" "$SCAN_PASSWORD"; then warn "Не удалось распаковать (возможно, зашифрованный архив)" note_encrypted "$out_dir" "$path" "failed to extract (maybe encrypted/wrong password)" return 0 fi scan_target "$exdir" "$out_dir" "$quick" "$nosbom" else warn "7z не смог прочитать архив — отмечаю как непрочитанный" note_encrypted "$out_dir" "$path" "unreadable archive" fi else if [[ -f "$path" ]]; then local ft ft=$(file -b "$path" || echo "") if grep -qiE "encrypted|pgp|gpg|openssl|s\/mime" <<<"$ft"; then warn "Зашифрованный файл: $path ($ft)" note_encrypted "$out_dir" "$path" "$ft" fi fi scan_target "$path" "$out_dir" "$quick" "$nosbom" fi } # ---- main ---- OUT_DIR="${DEFAULT_OUT}" QUICK="0" NOSBOM="0" ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --install) do_install; exit 0 ;; --help|-h) usage; exit 0 ;; -o|--out) OUT_DIR="$2"; shift 2 ;; --password) SCAN_PASSWORD="$2"; shift 2 ;; --no-sbom) NOSBOM="1"; shift ;; --quick) QUICK="1"; shift ;; --no-freshclam) FRESHCLAM_AUTO="0"; shift ;; --max-bytes) MAX_TARGET_BYTES="$2"; shift 2 ;; --) shift; break ;; -*) err "Неизвестная опция: $1" usage; exit 2 ;; *) ARGS+=("$1"); shift ;; esac done if ((${#ARGS[@]}==0)); then usage exit 2 fi mkdir -p "$OUT_DIR" create_yara_rules "$OUT_DIR" # Проверка зависимостей if ! check_deps; then warn "Запустите авто-установку: $(basename "$0") --install" exit 2 fi # Обновление баз ClamAV (опционально) if [[ "${FRESHCLAM_AUTO}" == "1" ]]; then if command -v freshclam >/dev/null 2>&1; then log "Обновляю базы ClamAV..." if freshclam >/dev/null 2>&1; then ok "Базы обновлены." else warn "freshclam без sudo может не работать. Попробуйте: sudo freshclam" fi fi fi # Скан каждого пути/архива for p in "${ARGS[@]}"; do if [[ ! -e "$p" ]]; then warn "Путь не найден: $p" continue fi rp="$( (command -v realpath >/dev/null 2>&1 && realpath "$p") || echo "$p" )" scan_archive_or_item "$rp" "$OUT_DIR" "$QUICK" "$NOSBOM" done summarize_findings "$OUT_DIR" # ====== Итоговый код выхода ====== CRIT=0 WARNINGS=0 # ClamAV FOUND → критично grep -Rqs "FOUND" "$OUT_DIR"/clamav_*.txt 2>/dev/null && CRIT=2 # YARA: любое срабатывание → критично if ls "$OUT_DIR"/yara_*.txt >/dev/null 2>&1; then grep -Rqs . "$OUT_DIR"/yara_*.txt 2>/dev/null && CRIT=2 fi # Shell behavior: CRITICAL → exit 2; HIGH/MEDIUM → предупреждение if command -v jq >/dev/null 2>&1; then if ls "$OUT_DIR"/shell_behavior_findings_*.json >/dev/null 2>&1; then if jq -e -s 'flatten | any(.severity=="CRITICAL")' \ "$OUT_DIR"/shell_behavior_findings_*.json >/dev/null 2>&1; then CRIT=2 fi if jq -e -s 'flatten | any(.severity=="HIGH" or .severity=="MEDIUM")' \ "$OUT_DIR"/shell_behavior_findings_*.json >/dev/null 2>&1; then WARNINGS=1 fi fi fi # Semgrep/Secrets/OSV — предупреждения только при ненулевых находках if command -v jq >/dev/null 2>&1; then # SEMGREP for f in "$OUT_DIR"/semgrep_*.json; do [[ -f "$f" ]] || continue cnt=$(jq '[.results[]?] | length' "$f" 2>/dev/null || echo 0) (( cnt > 0 )) && WARNINGS=1 done # GITLEAKS for f in "$OUT_DIR"/gitleaks_*.json; do [[ -f "$f" ]] || continue cnt=$(jq '.leaks | length' "$f" 2>/dev/null || echo 0) (( cnt > 0 )) && WARNINGS=1 done fi grep -Rqs "^ID:\s" "$OUT_DIR"/osv_*.txt 2>/dev/null && WARNINGS=1 if [[ $CRIT -eq 2 ]]; then err "Обнаружены КРИТИЧНЫЕ индикаторы. См. ${OUT_DIR}/SUMMARY.txt" exit 2 fi if [[ $WARNINGS -eq 1 ]]; then warn "Есть предупреждения. См. ${OUT_DIR}/SUMMARY.txt" exit 1 fi ok "Ничего подозрительного не найдено." exit 0