Началось с того, что родственники в очередной раз позвонили: «компьютер тормозит». Я приехал, открыл диспетчер задач и за десять минут нашёл всё то же самое, что находил в прошлые разы. Неподписанный exe со случайным именем в AppData. Процесс svchost.exe, который почему-то лежит в «Загрузках». Папка с документами, открытая по сети для всех. Файл пароли.txt на рабочем столе. Ничего хитрого, обычный набор. Я подумал, что эти десять минут можно автоматизировать, но так, чтобы результат понял человек, который не знает, что такое автозагрузка.
Получилась программа «Страж ПК». Это не антивирус: у неё нет базы сигнатур, и она ничего не удаляет без явного «Да». В статье я подробно расскажу, как она устроена: как из подписи, места запуска и командной строки получается оценка подозрительности, как найти связь с майнинг-пулом без вирусной базы, почему нельзя просто открыть файл из облака, как сделаны карантин, сжатие фото и видео, сейф и автообновление. Исходный код открыт (MIT), все фрагменты ниже взяты из него.

Скачать
Если хочется сначала посмотреть, а читать потом:
-
страница релиза, там всё: github.com/OlegChe77/strazh-pc/releases/latest;
-
установщик (около 150 МБ, обновляется сам, права администратора не нужны): StrazhPC-Setup-0.2.0.exe;
-
портативная версия без установки (около 138 МБ): StrazhPC-Portable-0.2.0.exe;
-
исходный код архивом (zip) для тех, кто хочет собрать сам: strazh-pc-0.2.0.zip, последняя версия ветки main: main.zip.
Важная оговорка: программа пока не подписана цифровой подписью, потому что сертификат стоит денег, а проект бесплатный. Поэтому Windows SmartScreen может показать окно «Windows защитила ваш компьютер»: нажмите «Подробнее» и «Выполнить в любом случае». Некоторые антивирусы осторожничают с новыми неподписанными сборками, и это ложное срабатывание. Исходники открыты, собрать программу можно самому, как именно — в конце статьи.
Что это и чем оно не является
«Страж ПК» делает две вещи. Первая: проверяет компьютер на то, что обычно бывает не так (вредные и подозрительные программы, майнеры, открытые наружу папки и порты, пароли на виду, сомнительные расширения браузера) и объясняет находки простыми словами. Вторая: освобождает место на диске, сжимая фото и видео и находя одинаковые файлы.
Что в нём принципиально:
-
Ничего не удаляется и не переносится без явного «Да». Опасное уезжает в карантин и возвращается одной кнопкой. Оригиналы фото и видео сначала не трогаются вообще, создаются копии.
-
Нет собственной базы вирусов. Программа смотрит на обстоятельства: подпись, место запуска, имя, параметры, нагрузку, подключения. Поэтому она не заменяет антивирус, и в интерфейсе об этом прямо сказано.
-
Нет регистрации и телеметрии. Данные остаются на компьютере. Единственное обращение в сеть: проверка новой версии на GitHub, её можно выключить.
-
Язык человека, а не специалиста. Каждая находка получает светофор («Опасно», «Стоит посмотреть», «Не страшно») и три пояснения: что это, чем опасно, что советую. Путь к файлу и технические детали спрятаны под «Для знающих».

Стек и устройство
Программа написана на Electron (версия 45), для сборки нужен Node.js 22 и новее, интерфейс на чистом HTML, CSS и JavaScript без фреймворков. Зависимостей в рантайме три: electron-updater, sharp (сжатие картинок) и ffmpeg-static (сжатие видео). Всё остальное: работа с реестром, планировщиком, сетью, сертификатами, SMB-папками, — это PowerShell, который запускается из Node.
Структура каталога src:
-
src/main/— главный процесс: окно, IPC, проверки угроз (threats/), поиск и сжатие (space/), сейф (vault.js), карантин (quarantine.js), обновления (updater.js); -
src/renderer/— интерфейс; -
src/main/preload.js— мост между ними.
Мост устроен по принципу «разрешено только то, что перечислено». Рендерер не получает ни ipcRenderer, ни Node. В preload.js лежит белый список каналов (scan:all, finding:act, quarantine:restore, space:compress, vault:unlock, organize:apply, update:install и так далее), и любой вызов вне списка отклоняется с ошибкой «Неизвестный вызов». Для программы, которая трогает файлы пользователя, это не лишняя предосторожность: интерфейс показывает чужие имена файлов и тексты расширений, и лучше, чтобы даже теоретически через него нельзя было вызвать лишнее.
PowerShell как слой доступа к системе
Почти все проверки — это вызовы PowerShell. Подписи файлов (Get-AuthenticodeSignature), общие папки (Get-SmbShare), порты и брандмауэр (Get-NetTCPConnection, Get-NetFirewallProfile), планировщик (Get-ScheduledTask), процессы (Get-Process, Get-CimInstance Win32_Process). Из Node их проще и надёжнее вызывать, чем тянуть нативные модули, которые потом надо собирать под каждую версию Electron.
Две практические вещи, на которых я потерял время.
Всегда просите JSON. Скрипт заканчивается на ConvertTo-Json -Compress, и Node разбирает ответ. Но у ConvertTo-Json есть известная неприятность: если результат состоит из одного объекта, он приходит не массивом, а объектом. Поэтому на стороне Node стоит маленькая обёртка asArray, которая приводит всё к массиву. Без неё проверка работает на машине с десятью автозапусками и ломается на чистой системе с одним.
Данные в скрипт нельзя вставлять строкой. Список из нескольких сотен путей не влезает в командную строку, да и с кавычками и русскими буквами начинается ад. Я пишу данные во временный JSON-файл, а скрипт читает его. Эту обёртку (withData) используют проверки, которым нужно передать в PowerShell список файлов. После работы файл удаляется.
Главная идея: оценивать не файл, а обстоятельства
Без базы сигнатур с настоящими антивирусами тягаться глупо. Зато многие плохие программы выдают себя не содержимым, а обстоятельствами. Для каждой программы я собираю четыре группы признаков:
-
Цифровая подпись. Подписан ли файл, цела ли подпись, кто издатель. Статус
HashMismatchозначает, что файл изменили после подписи. -
Где лежит. Временные папки,
AppData, «Загрузки», «Рабочий стол», «Документы»,ProgramData,C:UsersPublic. Туда может писать обычный пользователь без прав администратора, значит, и вредная программа. -
Имя. Совпадает ли с известным майнером, не притворяется ли системным файлом, не похоже ли на случайный набор букв и цифр.
-
Командная строка и нагрузка на процессор.
Из них складывается оценка bad, warn или ok и список причин. Сами причины тоже написаны для человека («Называется как служебный файл Windows, но лежит не там, где положено, и без цифровой подписи»). Ядро оценки — функция assess:
function assess({ exe, cmd = '', sig, cpu = 0 }) {
const reasons = [];
let level = null;
const up = (l, why) => { /* повысить уровень, запомнить причину */ };
if (minerName(exe)) up('bad', 'Название файла совпадает с известной программой для добычи криптовалюты.');
if (MINER_CMD.test(cmd)) up('bad', 'Запущена с параметрами, которые используют майнеры.');
if (BAD_CMD.test(cmd)) up('warn', 'Запускается скрытой командой — так обычно прячут вредный код.');
if (status === 'HashMismatch') up('bad', 'Файл изменили после подписи.');
if (mimic && !signed) up('bad', 'Называется как служебный файл Windows, но лежит не там и без подписи.');
else if (mimic && signed && !microsoft(sig.signer)) up('warn', 'Подписан не Microsoft и лежит не в папке Windows.');
if (!signed) {
if (user) {
if (cpu >= 25) up('bad', 'Без подписи, в папке пользователя и сильно нагружает компьютер.');
else if (randomName(exe)) up('bad', 'Без подписи, в папке пользователя, имя из случайных символов.');
else up('warn', 'Без подписи и в папке пользователя.');
} else if (cpu >= 25) up('warn', 'Без подписи и сильно нагружает компьютер.');
else if (!isSystemPath(exe)) up('ok', 'Без подписи. Это бывает у небольших, но честных программ.');
} else if (cpu >= 60) up('ok', 'Сильно нагружает компьютер, но программа подписана.');
return level ? { level, reasons } : null;
}
Ни один признак сам по себе ничего не значит. Неподписанных честных программ полно, особенно у небольших авторов (этот самый «Страж» тоже пока без подписи, что вышло немного смешно). Поэтому «Опасно» выставляется только на сочетании: нет подписи, лежит там, где пишет пользователь, и вдобавок нагружает процессор или называется набором мусора. Одиночный признак даёт «Стоит посмотреть» или вообще «Не страшно». Это резко сокращает число ложных тревог. Для такой программы ложная тревога хуже пропуска: человек один раз испугается зря и перестанет верить.
Имена: майнеры и подделки
Имя проверяется тремя функциями. minerName сравнивает имя файла со списком из нескольких десятков известных майнеров (xmrig, nbminer, t-rex, phoenixminer, lolminer и так далее). randomName считает «случайным» имя из десяти и более латинских букв и цифр, где есть хотя бы три цифры. mimicsSystem ловит старый трюк: файл называют svchost.exe, lsass.exe или csrss.exe и кладут не в System32. Чтобы не обходилось добавлением цифры в конец, у имени отбрасывается хвост из цифр, подчёркиваний и пробелов: svchost32 и lsass_ тоже считаются подделками.
Если имя системное, файл лежит вне системных папок и без подписи, это bad. Если подписан, но не Microsoft, это warn: на практике так выглядят и честные программы с неудачно выбранным именем, и аккуратная подделка с купленным сертификатом.
Командные строки
Майнеры и загрузчики часто видны по параметрам запуска, даже если сам файл назван невинно. Два регулярных выражения. Первое, MINER_CMD, ищет типичное для майнеров: адреса stratum+tcp://, --donate-level, --coin, --algo, cryptonight, randomx, параметр -o с адресом пула. Второе, BAD_CMD, ищет приёмы скрытого запуска: -EncodedCommand, -WindowStyle Hidden, DownloadString, Invoke-Expression и iex, FromBase64String, mshta со ссылкой, regsvr32 /i:http, certutil -urlcache, bitsadmin /transfer. Командная строка берётся через Get-CimInstance Win32_Process.
Здесь есть ловушка: Get-Process не отдаёт путь к файлу для процессов, к которым нет доступа (обычно системных). Такие процессы программа пропускает, потому что оценивать там нечего: системные папки она в любом случае не трогает.
Подписи массово
Проверять подпись каждого файла отдельным запуском PowerShell было бы мучительно долго: старт скрипта занимает заметное время. Поэтому все пути из одной проверки собираются в один список, и один скрипт за один проход вызывает Get-AuthenticodeSignature для каждого файла и возвращает статус и издателя (поле Subject сертификата). Издателя я достаю регуляркой по полю CN. Подписанным считается только статус Valid. Любой другой (NotSigned, HashMismatch, UnknownError) — неподписанный в смысле оценки, а отдельный разговор идёт про HashMismatch.
Автозагрузка и планировщик
Автозапуск — главное место, где вредные программы закрепляются. Скрипт собирает программы из трёх источников:
-
ключи
RunиRunOnceв реестре:HKCUиHKLM, плюс веткаWOW6432Nodeдля 32-битных программ; -
папки «Автозагрузка» (пользовательская и общая); ярлыки
.lnkразворачиваются черезWScript.Shellв целевой файл и аргументы; -
задания планировщика, кроме тех, что лежат в
Microsoft*, и отключённых.
Из строки автозапуска надо достать путь к файлу, а это не так просто, как кажется: путь бывает в кавычках, бывает без кавычек, но с пробелами в имени папки и с аргументами после. Функция extractExe сначала подставляет переменные окружения вроде %APPDATA%, потом пробует взять первую строку в кавычках, потом ищет первое вхождение любого из известных расширений (.exe, .bat, .cmd, .vbs, .ps1, .dll, .scr, .lnk и других) и режет по нему. В оценку при этом уходит вся командная строка целиком. Так скриптовые оболочки (powershell.exe -enc ...) проверяются по аргументам: сам powershell.exe подписан Microsoft, а вот что именно он запускает, видно только по параметрам.
Отдельная деталь про задание «копии раз в неделю», которое создаёт сама программа. Без исключения оно попало бы в находки как «задание планировщика, которое запускает неподписанный файл». Поэтому есть проверка isOwnTask: имя задания должно подходить под шаблон, а путь запуска — совпадать с путём самой программы или лежать в её папке. Чужое задание с тем же именем под исключение не попадает.
Работающие программы и замер нагрузки
Нагрузка в процентах — это не поле, а разность двух замеров. Get-Process отдаёт накопленное процессорное время, поэтому скрипт читает значения, ждёт 2,5 секунды, читает снова и делит разницу на время и на число ядер:
$n=[Environment]::ProcessorCount
$a=@{}; Get-Process | % { if($_.CPU -ne $null){ $a[$_.Id]=$_.CPU } }
Start-Sleep -Milliseconds 2500
$b=Get-Process
# cpu = (($p.CPU - $a[$p.Id]) / 2.5 / $n) * 100
Почему 2,5 секунды, а не секунда? Это компромисс: процессорное время дискретно, и на очень коротком интервале короткий всплеск легко принять за постоянную нагрузку. Большой интервал делает проверку заметно медленнее, а она и так идёт десятки секунд.
Дальше важная мелочь. Один и тот же exe часто запущен несколько раз: браузеры, хелперы, а ещё майнер, который разбивает работу на много процессов по несколько процентов. Если оценивать каждый процесс по отдельности, такой майнер никогда не пройдёт порог. Поэтому процессы группируются по пути файла, нагрузка суммируется, а номера процессов сохраняются, чтобы потом, если пользователь согласится, можно было завершить именно их. Системные пути и сама программа из проверки исключаются.
Сетевые подключения и майнинг-пулы
Майнер можно переименовать, переупаковать, подписать чужим сертификатом, но ему всё равно нужно ходить на пул. Это и ловит проверка сети. Установленные исходящие подключения (Get-NetTCPConnection -State Established) отфильтровываются от локальных адресов (10.*, 192.168.*, 172.16–31.*, 127.* и других частных диапазонов). Для каждого внешнего подключения проверяются два признака:
-
Адрес входит в известный пул. Программа резолвит DNS-имена нескольких десятков известных пулов (
pool.supportxmr.com,xmr.nanopool.org,eth.2miners.com,pool.hashvault.pro,rx.unmineable.com,stratum.braiins.comи многих других) и собирает множество их IP-адресов. Каждый резолв ограничен таймаутом 2,5 секунды, чтобы один недоступный DNS не растягивал проверку. -
Порт «майнерский». Порты 3333, 4444, 5555, 7777, 9999, 14444 и ещё несколько часто используют пулы. Сам по себе порт ничего не доказывает, поэтому по IP делается обратный DNS-запрос, и если в имени узла встречаются слова вроде
pool,xmr,monero,nanopool,2miners,f2pool,stratum, совпадение считается сильным. Без такого слова остаётся слабое.
«Сильное» совпадение даёт bad. «Слабое» (просто «майнерский» порт) даёт только warn, и то лишь если сама программа выглядит подозрительно: нет подписи, лежит в папке пользователя или грузит процессор от 25%. Идея та же: один признак — не приговор.
Общие папки и открытые порты
Общие папки. Get-SmbShare отдаёт сетевые папки, административные (с $ на конце) и специальные отбрасываются. Дальше смотрим права доступа через Get-SmbShareAccess и ищем среди разрешённых учётные записи «Все» (Everyone), анонимные, гостевые и «прошедшие проверку». Если такая запись может ещё и менять файлы (Full или Change), это bad, если только читать, то warn. Регулярка рассчитана и на английские, и на русские названия групп, потому что в русской Windows Everyone превращается во «Все».
Открытые порты — более тонкая проверка, потому что слушающих портов на любой машине десятки, а проблемны единицы. Что считается:
-
только порты, которые слушают не на
127.0.0.1и не на::1, то есть доступные не только с этого компьютера; -
известные службы по номеру порта: удалённый рабочий стол (3389), VNC (5800, 5900–5903), telnet (23), FTP (21, 990), SSH и популярные средства удалённого доступа, базы данных (MSSQL 1433, MySQL 3306, PostgreSQL 5432, Redis 6379, Elasticsearch 9200, MongoDB 27017 и другие), открытый API Docker (2375) и «веб-порты» (80, 443, 8080 и подобные);
-
у каждой службы есть исходный уровень (telnet и открытый Docker сразу
bad, остальныеwarn) и признакsignedOk: подписанная программа вне папок пользователя на таких портах не вызывает тревоги.
Чтобы не завалить пользователя шумом, служебные порты Windows (svchost, lsass, spoolsv и подобные) не показываются, пока порт не входит в список известных служб. Но и там есть тонкость: у служб Windows «веб-портами» считаются только 80, 443, 8080 и 8443, потому что на 5000 и подобных сидят служебные UPnP-службы, и показывать их как «открытый веб-сервер» значило бы пугать зря.
Отдельно учитывается состояние защиты. Скрипт читает профили брандмауэра (Get-NetFirewallProfile), активные сетевые профили (Get-NetConnectionProfile), а для удалённого рабочего стола ещё и ключи реестра UserAuthentication (проверка на уровне сети) и fDenyTSConnections. Открытый RDP при включённом брандмауэре и NLA выглядит совсем иначе, чем при выключенном брандмауэре в общедоступной сети, и объяснение для пользователя меняется вместе с этим.
Файлы только в облаке
Эта проверка переписывалась у меня дважды. Идея простая: найти в OneDrive, «Яндекс.Диске», Google Диске, Dropbox и «Облаке Mail.ru» файлы с паролями и ключами. Но современные облака умеют держать файлы «по запросу»: имя в папке есть, а на диске содержимого нет. Если просто прочитать такой файл, чтобы поискать в нём пароль, облачный клиент начнёт его скачивать. Проверка превращается в скачивание гигабайтов, а на медленном интернете ещё и в зависший компьютер.
Выручают атрибуты файла. Windows помечает такие файлы флагами Offline, RecallOnOpen и RecallOnDataAccess. Я читаю их через [IO.File]::GetAttributes и пропускаю такие файлы, не открывая:
const OFFLINE_MASK = 0x1000 | 0x40000 | 0x400000; // Offline | RecallOnOpen | RecallOnDataAccess
// strict: если атрибуты прочитать не удалось, считаем ВСЕ файлы облачными —
// лучше не открыть, чем запустить скачивание.
function makeOfflineFilter({ strict = false } = {}) { /* ... */ }
Режим strict включается для облачных папок. Если PowerShell упал или не ответил, то безопаснее решить, что все файлы облачные, и не читать ни один, чем рискнуть запустить массовое скачивание. Для обычных папок режим мягкий: не удалось узнать, значит, считаем файлы обычными. По именам такие файлы всё равно проверяются, содержимое не читается. В итоге пользователю честно говорится, что часть файлов не смотрели, потому что они лежат только в облаке.
Пароли и ключи на виду
Проверка идёт по «Документам», «Рабочему столу» и облачным папкам, вглубь до четырёх уровней, с ограничением в 40 тысяч файлов за проход. Служебные папки (node_modules, .git, AppData, корзина, собственные папки сжатых копий) пропускаются, символические ссылки тоже, чтобы не уйти в цикл.
Файл попадает в подозрительные по одному из трёх признаков:
-
по имени: слова
пароль,пароли,логин,password,passwd,credentials,секрет,secret,seed,mnemonic,wallet,кошелёк,ключи,token,pinв имени файла с подходящим расширением (текст, таблицы, документы, PDF); -
по расширению ключа:
.pem,.key,.ppk,.pfx,.p12,.ovpnили имена видаid_rsa,id_ed25519; -
по содержимому: в текстовых файлах до 512 КБ ищутся строки вида «пароль: что-то» и блоки
-----BEGIN ... PRIVATE KEY-----.
Регулярка по строкам пароля отсекает заглушки: password: ****, xxx, ___, <...> и ... не считаются находкой. Иначе каждый шаблон документации превращался бы в «опасно». Файлы больше 512 КБ по содержимому не читаются, но по имени всё равно попадают в список. Сами найденные пароли в интерфейсе не показываются: программа говорит, что в файле есть что-то похожее на пароль, и объясняет, чем это опасно.
Расширения браузеров
Расширения — отдельная тема, потому что именно через них сегодня нередко крадут сессии и подменяют рекламу. Программа поддерживает Chrome, Edge, Brave, Vivaldi, Яндекс Браузер, Opera и Opera GX, а для Firefox читает extensions.json. Для браузеров на Chromium она обходит папки профилей, читает manifest.json каждого расширения (с учётом локализации __MSG_...__ через _locales) и считает баллы.
Баллы начисляются за права, которые легко объяснить человеку: читать «куки» (на них держится вход в аккаунты), историю, адреса всех вкладок, буфер обмена, снимать экран и вкладки, менять запросы браузера на лету, общаться с программами на компьютере, менять прокси, управлять другими дополнениями. Два балла дополнительно получает право читать и менять все сайты (<all_urls> и родственные шаблоны). Отдельно учитывается происхождение: расширение, установленное из распакованной папки или внешним способом, подозрительнее, чем из магазина. Каждое право превращается в предложение на человеческом языке («Может читать историю посещённых сайтов»), а решение, отключить расширение или оставить, всегда за пользователем.

Карантин вместо удаления
Принцип проекта: пользователь не должен бояться кнопки. Поэтому «Обезвредить» ничего не стирает. Файл переезжает в закрытую папку программы под случайным UUID-именем с расширением .quar, а рядом в manifest.json пишется запись: откуда файл, почему убран, дата, размер, SHA-256 (для файлов до 300 МБ), а также какие записи автозапуска в реестре и какие задания планировщика были с ним связаны.
Манифест пишется атомарно: сначала во временный файл manifest.json.tmp, потом переименованием поверх основного. Если питание пропадёт посередине записи, останется либо старая, либо новая целая версия, а не оборванный JSON. Восстановление возвращает файл на старое место и заново создаёт убранные записи автозапуска. Карантин принципиально не принимает файлы из системных папок Windows: проверка стоит прямо в функции добавления, а не только в интерфейсе.
Сжатие фото и видео
Вторая половина программы нужна почти каждому: диск забит снимками с телефона, видео и старыми установщиками. Поиск работает только в папках пользователя. Системные папки, AppData, собственные папки программы и карантин не сканируются, а глубина обхода и число просмотренных файлов ограничены. Кандидатами на сжатие считаются файлы от определённого размера: JPEG от 300 КБ, PNG от 200 КБ, видео от 30 МБ, потому что с маленькими файлами возни больше, чем выигрыша.

Фото сжимаются через sharp: JPEG через mozjpeg с качеством 80, PNG с палитрой, с поворотом по EXIF и ограничением длинной стороны 4096 пикселей, без увеличения. Метаданные сохраняются.
Видео обрабатывает встроенный ffmpeg-static: H.264, -crf 27, аудио AAC 128 кбит/с, +faststart:
const args = ['-hide_banner', '-nostdin', '-y', '-i', item.path,
'-map', '0:v:0', '-map', '0:a?',
'-c:v', 'libx264', '-preset', 'medium', '-crf', '27', '-pix_fmt', 'yuv420p',
'-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-f', 'mp4', tmp];
Прогресс берётся из вывода ffmpeg: при старте ловится Duration:, дальше разбираются строки time=, и по ним считается процент.
Оригинал никогда не заменяется. Копия сначала пишется в отдельную папку (с сохранением структуры исходных папок) во временный файл с суффиксом .part. Для видео результат ещё и проверяется: длительность копии должна совпадать с исходной с допуском 2% (но не меньше двух секунд). Если ffmpeg завершился с нулевым кодом, а видео получилось короче, копия выбрасывается, потому что плохую копию лучше потерять, чем заменить ею оригинал. Только после такой проверки и явного согласия пользователя оригинал отправляется в Корзину.
У Electron тут свой подвох. Бинарник ffmpeg нельзя запускать из app.asar, поэтому в настройках сборки node_modules/ffmpeg-static/** вынесен в asarUnpack, а в коде путь подменяется с app.asar на app.asar.unpacked. Без этого в разработке всё работает, а в собранной программе видео не сжимается.
Одинаковые файлы
Поиск дублей устроен как воронка, чтобы не хешировать терабайты. Кандидаты — личные файлы (картинки, видео, музыка, документы, архивы, образы) от 100 КБ. Сначала они группируются по точному размеру, потому что у файлов разного размера совпадений быть не может. Внутри группы считается хеш первых 64 КБ, и только если он совпал, считается полный SHA-256. Основная масса файлов отсеивается на первых двух шагах и никогда не читается целиком.
Раскладка по папкам «год/месяц»
Отдельная функция раскладывает фото и видео по папкам вида По датам2026Октябрь. Дата берётся по приоритету: сначала из EXIF снимка, потом из имени файла (шаблоны вида IMG_20230415_…, 2023-04-15 12.30.00, Screenshot_2023-04-15), и только потом из даты файла. EXIF разбирается без сторонних библиотек: это чтение TIFF-структуры внутри JPEG (теги DateTimeOriginal, DateTimeDigitized, DateTime) с учётом порядка байтов II/MM. Дата отбрасывается, если год вне разумных пределов (раньше 1990 или позже следующего).
Раскладываются только папки, где телефоны и Windows обычно сваливают снимки россыпью: Camera Roll, DCIM, Screenshots, «Снимки экрана», WhatsApp Images, Telegram. Альбомы, поездки и любые папки, которые человек назвал сам, не трогаются: у такой структуры уже есть смысл. Файлы только переносятся, никогда не удаляются и не перезаписываются, а каждый перенос пишется в журнал, поэтому работает кнопка «Вернуть как было».
Копии раз в неделю
Для тех, кому лень нажимать кнопку, есть недельное задание. Оно создаётся в Планировщике Windows (Register-ScheduledTask с триггером Weekly) и запускает программу с ключом --weekly. Настройки задания продуманы под ноутбуки: StartWhenAvailable (если компьютер был выключен, задание выполнится при включении), ограничение времени десять часов, запрет параллельных запусков. Фоновая работа только создаёт сжатые копии. О замене оригиналов программа спросит, когда пользователь сам её откроет.
Сейф
Сейф хранит заметки с паролями и файлы в зашифрованном виде. Параметры такие: AES-256-GCM, ключ получается из пароля через scrypt (N=32768, r=8, p=1), минимальная длина пароля восемь символов, ключ живёт только в памяти процесса, а через десять минут бездействия сейф закрывается сам. Пароль нигде не хранится, поэтому восстановить его нельзя. Это сознательный выбор, и он честно написан в интерфейсе: любой механизм «сброса» означал бы чёрный ход.
Формат файла простой: четыре байта сигнатуры SPK1, 12 байт случайного вектора, шифртекст и 16 байт тега аутентификации в конце. Большие файлы шифруются потоком, без чтения целиком в память, а хеш SHA-256 исходного файла считается на лету. При расшифровке тег читается заранее из конца файла, так что, если файл хоть немного изменили, расшифровка завершается ошибкой, и повреждённые данные не отдаются как настоящие.
Файлы можно добавлять в сейф, после чего программа спрашивает, убрать ли оригиналы в Корзину или оставить как есть. Сама она ничего не убирает без этого ответа.
Автообновление и сборка
Для сборки используются electron-builder и два артефакта: установщик NSIS и портативная версия. Обновление идёт через electron-updater с GitHub Releases. Тут есть решение, ради которого я отказался от привычного поведения. Обычно апдейтер сам скачивает и ставит новую версию в фоне. Я выставил autoDownload = false и autoInstallOnAppQuit = false: программа только сообщает «Вышла новая версия, [Обновить]», а скачивание и установка начинаются после нажатия. Для программы, которая сама говорит пользователю «не доверяйте лишнему», это вопрос последовательности.
Портативная версия обновляться на месте не может, поэтому при нажатии она просто открывает страницу загрузки. Проверку обновлений можно выключить или запустить вручную («Ещё → Обновления»).
Релиз собирает GitHub Actions: на push тега v* запускается сборка на windows-latest. Сначала идёт npm run selftest (самопроверка без окна), и только потом npm run release, который собирает установщик, portable и latest.yml и публикует их в релиз. Чтобы выпустить новую версию, достаточно поднять version в package.json, поставить тег командой git tag v0.2.1 и отправить его через git push origin main v0.2.1.
Отдельная мелочь в build/installer.nsh. Стандартный установщик проверяет, не запущена ли программа, через WMI, а на компьютерах со сломанным WMI эта проверка не работает. Я заменил её закрытием процесса без WMI. Пустяк, но именно на таких «странных» компьютерах программа и нужна больше всего.
Сборка из исходников и тесты
Для сборки нужны Node.js 22+ и Windows. Порядок такой: npm install, затем npm start для запуска, npm run selftest для проверки без окна и npm run dist, чтобы собрать установщик и portable в папку dist/. Скриншоты для README делает node scripts/shots.js на вымышленных данных, поэтому на них нет чьих-то реальных файлов и имён.
В scripts/ лежат самопроверка (selftest.js), сквозной прогон (e2e.js), интерфейсная проверка и генератор скриншотов. Самопроверка запускается и в GitHub Actions перед сборкой релиза, так что сломанная версия до пользователей не доедет.

Что не получилось и чего здесь нет
-
Подписи кода пока нет. SmartScreen ругается, часть антивирусов относится к сборке с подозрением. Сертификат стоит денег.
-
Это эвристика, а не детектор. Хорошо замаскированная вредная программа с настоящей подписью (например, на украденном сертификате) пройдёт. Для моей задачи (найти очевидное на компьютере родственника) этого достаточно, но защитой от целевых атак программа не является, и я не хочу, чтобы её так воспринимали.
-
Только Windows 10 и 11 и русский интерфейс.
-
Замер нагрузки раз в 2,5 секунды может не поймать майнер, который включается раз в несколько минут. Здесь лучше работает проверка автозагрузки и сетевых подключений.
-
Проверка сети зависит от списка пулов. Он вшит в программу и обновляется вместе с ней. Новый пул, которого в списке нет, можно найти только по «майнерскому» порту и обратному DNS.
Где взять и что дальше
Установщик и портативная версия: github.com/OlegChe77/strazh-pc/releases/latest. Прямые ссылки: установщик, portable, исходники zip. Репозиторий под лицензией MIT: github.com/OlegChe77/strazh-pc.
Это первая публичная версия, и мне важно понять, каких проверок не хватает. Расскажите в комментариях, какие «странные» вещи вы находили на компьютерах родственников и знакомых: майнеры под видом системных процессов, открытый RDP, пароли в текстовых файлах, расширения-шпионы. Добавлю такие проверки. Ошибки и идеи можно оставлять в Issues.
Автор: OlegChe77


