Автоматизация мониторинга JavaScript-файлов для Bug Bounty. Часть 2. Crawler
Ознакомьтесь с первой частью статьи, где рассказывается об проблеме автоматизации мониторинга JS-файлов на наличие новых фич и общей методологии их обнаружения. В этой части мы перейдем к практике и разберем код реализации.Начнем с модуля, отвечающего за сбор трафика с нужными нам JS-файлами, на примере баг-баунти таргета Udemy — https://hackerone.com/udemy/
Хантить там я порекомендовать не могу, но сервис отлично подходит для демонстрационных целей, поскольку использует и Cloudflare, и manifest-файлы. Это означает, что другими методами (без анализа трафика браузера) собрать все JS-файлы мы не сможем.
В качестве языка программирования используем Bun — современный и удобный движок для Node.js, поддерживающий TypeScript из коробки.
Установка camoufox
❯ pip install -U camoufox
❯ python3 -m camoufox fetch
Создание проекта
❯ git init .
❯ bun init
✓ Select a project template: Blank
+ CLAUDE.md
+ index.ts
+ tsconfig.json (for editor autocomplete)
+ README.md
To get started, run:
bun run index.ts
bun install v1.2.22 (6bafe260)
+ @types/[email protected]
+ [email protected]
7 packages installed [3.50s]
Установка зависимостей
❯ bun add playwright yargs @babel/parser @babel/traverse @babel/generator
bun add v1.2.22 (6bafe260)
installed [email protected] with binaries:
- playwright
installed [email protected]
installed @babel/[email protected] with binaries:
- parser
installed @babel/[email protected]
installed @babel/[email protected]
33 packages installed [4.49s]
Описание зависимостей:
- playwright — фреймворк для E2E-тестирования, который будем использовать для навигации по сайту и перехвата сетевого трафика
- yargs — библиотека для парсинга аргументов командной строки
- babel (@babel/parser, @babel/traverse, @babel/generator) — инструменты для разбора и анализа JS-файлов в поиске манифест-файлов
Создадим базовую структуру модуля crawler
❯ mkdir -p crawler/scenarios/manifest
❯ touch crawler/scenarios/{anon.ts,manifest.ts,manifest/manifest-parser.ts} crawler/{cli.ts,crawler.ts,scenario.d.ts}
❯ tree crawler/
crawler/
├── cli.ts
├── crawler.ts
├── scenario.d.ts
└── scenarios
├── anon.ts
├── manifest
│ └── manifest-parser.ts
└── manifest.ts
3 directories, 6 files
Описание файлов:
- cli.ts — точка входа в программу. Здесь парсим аргументы командной строки и передаем управление в crawler.ts
- crawler.ts — основной код, запускающий сценарии сбора JS-файлов
- scenario.d.ts — типы TypeScript для описания структуры сценариев
- anon.ts — сценарий для сбора JS-файлов без аутентификации. Описывает, какие страницы необходимо посетить
- manifest.ts — сценарий поиска webpack-манифеста. Посещает главную страницу сайта, ищет файл с манифестом webpack, формирует список URL всех модулей и открывает их в браузере для записи трафика
- manifest-parser.ts — вспомогательный модуль, содержащий парсер JS-кода, который извлекает названия всех модулей из манифеста
Файл cli.ts
Создаем новые директории:❯ mkdir -p revisions/{previous,current}
В директории revisions мы будем хранить данные (в контексте краулера — HAR-файлы) от нашей утилиты для различных версий: текущий запуск и предыдущий.
Также добавим их в .gitignore:
revisions/*
node_modules
Спойлер: Полный код файла crawler/cli.ts
#!/usr/bin/env bun
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { CrawlRevision, main } from './crawler';
import { existsSync, readdirSync } from 'fs';
const browserPath = `${process.env.HOME}/.cache/camoufox/camoufox-bin`;
const builder = (yargs) => {
return yargs
.option('scenarios', {
alias: 's',
type: 'array',
describe: 'Specify scenario to process (path to file or scenario name in scenarios directory with/without .ts extension)',
string: true,
})
.option('visible', {
alias: 'v',
type: 'boolean',
describe: 'Run browser in non-headless mode',
default: false
})
.option('browserPath', {
alias: 'b',
type: 'string',
describe: 'Path to browser executable',
default: browserPath
});
};
const handler = revision => (args) => {
main({ revision, ...args });
}
yargs(hideBin(process.argv))
.strict()
.scriptName('crawler')
.usage('Usage: $0 <revision> [options]')
.command({
command: 'current [options]',
aliases: ['c'],
describe: 'Run crawler and save HARs as current revision',
builder,
handler: handler(CrawlRevision.Current)
})
.command({
command: 'previous [options]',
aliases: ['p'],
describe: 'Run crawler and save HARs as previous revision',
builder: builder,
handler: handler(CrawlRevision.Previous)
})
.demandCommand(1, 'You must specify a command (current/c or previous/p)')
.help('h')
.alias('h', 'help')
.parseSync();
Код достаточно говорит сам за себя. Теперь примеры использования:
# Запуск сценариев и сохранение HAR-файлов в текущую ревизию
bun run crawler/cli.ts current
# Сохранение в предыдущую ревизию
bun run crawler/cli.ts previous
# Ключ -v отключает headless-режим, т.е. окно браузера Camoufox будет отображаться — полезно для дебага
bun run crawler/cli.ts current -v
# По умолчанию выполняются все сценарии, но можно указать конкретный, например:
bun run crawler/cli.ts current -s anon
Сценарии
Определим интерфейс экспорта файла сценария:// файл crawler/scenario.d.ts
import { Page } from "playwright"
export interface Scenario {
customScenario: (page: Page, options: Record<string, string>) => Promise<void>
urls: string[]
beforeEachUrl?: (page: Page, url: string) => Promise<void>
proxy?: string
headers: Record<string, string>
waitForTimeout?: number
}
Основные поля здесь — urls и customScenario.
Первой выполняется функция customScenario: она получает объект page от Playwright, который является интерфейсом для взаимодействия с браузером — переход по ссылкам, нажатие кнопок, заполнение форм и так далее. Конкретный пример будет дан в файле anon.ts.
urls — просто список ссылок, которые нам нужно открыть в браузере. Во многих случаях дополнительная логика не требуется, и это просто автоматизация того, что можно сделать через customScenario.
beforeEachUrl — функция, которая выполняется перед посещением каждой ссылки из urls. Используется для проставления заголовка E-Tag, чтобы избежать кэширования JS-файлов.
headers — здесь размещаем наши авторизационные куки.
waitForTimeout — пауза между посещением ссылок из urls.
Далее разберем базовый пример с посещением двух ссылок без авторизации. В реальности для полного покрытия этим ограничиваться нельзя, но дописать дополнительные сценарии труда не составит, посидев достаточно времени за Burp'ом и документацией Playwright.
Сценарий anon.ts
Как следует из названия, здесь мы просто посещаем страницы без авторизации. Для наглядности оставим только две ссылки — главная страница и форма логина.
Содержимое customScenario по сути дублирует функционал urls и оставлено только для наглядности и примера того, как можно использовать Playwright, т.к. клик по кнопке "Log in" перекинет нас на страницу /join/passwordless-auth.
Таймауты здесь расставлены с запасом и учетом моих довольно медленных прокси. Ну и как показывает практика, многие E2E-тесты только на таймаутах и держатся — стоит попытаться оптимизировать, все разваливается. Так что ничего с этим не поделать, подождем.
Спойлер: Полный код файла crawler/scenarios/anon.ts
import { Page } from 'playwright';
import { Scenario } from '../scenario';
const proxy = ''; // socks5://localhost:2080
const COOKIES = '';
const urls = [
'https://www.udemy.com',
'https://www.udemy.com/join/passwordless-auth/?locale=en_US&next=https%3A%2F%2Fwww.udemy.com%2F&response_type=html&action=login&mode',
];
async function customScenario(page: Page, options: Record<string, string>) {
/*
console.log('* Visiting https://www.udemy.com');
let url = 'https://www.udemy.com';
await page.goto(url, { waitUntil: 'load', timeout: 60000 });
await page.waitForTimeout(9424);
await page.getByText('Log in').click();
await page.waitForTimeout(9424);
*/
}
const headers = {'Cookie':COOKIES};
export default {
urls,
proxy,
customScenario,
headers,
} as Scenario
Сценарий manifest.ts
Опишем общий алгоритм работы. Сперва посещаем страницу логина, с которой подгружается webpack-манифест. Ставим коллбэк на ответы с сервера и ищем среди них манифест по подстроке entry-main-manifest в пути.
Далее получаем тело ответа, парсим код манифеста и извлекаем все ID модулей. С помощью функции fillUrls формируем список ссылок на все JS-файлы и складываем их в поле urls нашего сценария. Следом после customScenario, согласно реализации в crawler, будут посещены все ссылки из urls, которые мы добавили. С помощью beforeEachUrl отключаем кэширование, т.к. некоторые JS-файлы уже были загружены.
Спойлер: Полный код файла файла crawler/scenarios/manifest.ts
import { Page } from 'playwright';
import { Scenario } from '../scenario';
import { manifestParser } from './manifest/manifest-parser';
const COOKIE = '';
//const proxy = 'socks5://127.0.0.1:2080';
const proxy = '';
const headers = {}; // Cookie': COOKIE};
const urls: string[] = [];//.slice(0,1)
// базовый путь для модулей, извлеченных из манифеста
const base = 'https://www.udemy.com/staticx/udemy/js/webpack/';
const fillUrls = (obj) => {
Object.entries(obj).forEach(([key, value]) => {
if (!key.includes('-server-side')) { // дополнительная фильтрация неинтересущих нас файлов
const url = `${base}${key}.${value}.js`;
urls.push(url);
}
})
}
async function customScenario(page: Page, options: Record<string, string>) {
console.log('customScenario');
page.on('response', async response => {
const url = response.url();
if (!url.includes('entry-main-manifest')) {
return;
}
console.log('Downloaded manifest file', url);
try {
const bodyBuf = await response.body();
const body = new TextDecoder().decode(bodyBuf);
const obj = manifestParser(body);
fillUrls(obj);
console.log(`Found ${urls.length} URLs to download`);
} catch (err) {
console.error('Failed to read response body:', err);
}
});
console.log('Visiting main page to download manifest file');
await page.goto('https://www.udemy.com/join/passwordless-auth/', { waitUntil: 'networkidle', timeout: 60000 });
await page.waitForTimeout(1112);
}
const beforeEachUrl = async (page: Page, url: string) => {
function generateEtag() {
return Array.from({length: 32}, () => Math.floor(Math.random() * 16).toString(16)).join('');
}
await page.setExtraHTTPHeaders({
'If-None-Match': `W/"${generateEtag()}"`
});
}
export default {
urls,
proxy,
customScenario,
headers,
waitForTimeout: 1000,
beforeEachUrl
} as Scenario
Парсер манифеста
Чтобы понять, как работает парсер, сначала рассмотрим код самого манифеста и найдем участок, отвечающий за ID (хеш) модулей.
Скачиваем файл и форматируем его:
❯ wget https://www.udemy.com/s4taticx/udemy/js/webpack/entry-main-manifest.e080d99058059da9d5f0.js
❯ f="./entry-main-manifest.e080d99058059da9d5f0.js"; bunx js-beautify $f > $f.tmp && mv $f{.tmp,}
Открываем в редакторе кода и листаем, пока не наткнемся на такой код:
i.u = e => "" + e + "." + {
"common-app-css": "6aba0217b0532ace1dce",
"activity-notifications-server-side": "03d1a23e2dec29b5cc3e",
"activity-notifications-udlite-app": "36183e97e8a8a3d357be",
"assessment-not-available-udlite-app": "3cf544de4da61135d048"
<SNIP>
"node_modules_storybook_blocks_dist_Color-6VNJS4EI_mjs": "81308e96f40e9436df67",
"node_modules_storybook_components_dist_syntaxhighlighter-NMPM6SWI_mjs": "c44add36fd696ac4c558",
"vendors-node_modules_storybook_components_dist_formatter-SWP5E3XI_mjs": "798be7d81f01b9f4410b",
"node_modules_storybook_components_dist_WithTooltip-4HIR6TLV_mjs": "d0cb52092f343ab43026"
} [e] + ".js"
})();
Здесь определена функция, которая отображает название модуля в имя файла с помощью хешмапа, например:
assessment-not-available-udlite-app
assessment-not-available-udlite-app.3cf544de4da61135d048.js
Для обхода всех нод AST воспользуемся функцией traverse из библиотеки Babel:
traverse(ast, {
ObjectExpression(path) {
const properties = path.node.properties;
for (const prop of properties) {
if (prop.type === "ObjectProperty") {
Предварительно конвертируем все ключи, которые встретятся, в строку:
let keyValue = "";
if (prop.key.type === "Identifier") {
keyValue = prop.key.name;
} else if (prop.key.type === "StringLiteral") {
keyValue = prop.key.value;
} else if (prop.key.type === "NumericLiteral") {
keyValue = prop.key.value.toString();
}
Как нашли — генерируем из него валидный JavaScript-код и запускаем eval, так как в ключах или значениях могут присутствовать не только константы, но и выражения, например:
[some_var]: "string",
"asdf": "string" + "2",
eval выполнит их и вернет готовый JS object:
const objectCode = generator(path.node, {
compact: false,
minified: false
}).code;
const jsObject = eval(`(${objectCode})`);
Далее уберём из объекта неинтересующие нас JS‑файлы — например, библиотеки и содержимое node_modules:
for (const k of Object.keys(jsObject)) {
if (ignorePatterns.some(p => k.match(new RegExp(p)))) {
delete jsObject[k]
}
}
Хешмап со всеми данными для формирования списка URL с JS-файлами готов. Передаём его обратно в код сценария.
Спойлер: Полный код парсера
// file crawler/scenarios/manifest/manifest-parser.ts
import parser from "@babel/parser";
import traverse from "@babel/traverse";
import generator from "@babel/generator";
// регулярки для фильтрации ненужных нам файлов
const DEFAULT_PATTERNS = [
'^vendors-node_modules_',
'^react-syntax-highlighter_',
'^brace',
'^vendor-highcharts',
]
const KEY_MATCH = '_mjs'
export const manifestParser = (code: string, ignorePatterns = DEFAULT_PATTERNS) => {
try {
const ast = parser.parse(code, {
sourceType: "unambiguous",
allowReturnOutsideFunction: true,
allowAwaitOutsideFunction: true,
errorRecovery: true,
// список плагинов для расширения синтаксиса, поддерживаемого babel
// много из списка ниже не пригодится в данном случае можно убрать для увеличения производительности, но сейчас это некритично
plugins: [
"jsx",
"classProperties",
"dynamicImport",
"optionalChaining",
"nullishCoalescingOperator",
"objectRestSpread"
]
});
let result = {}
traverse(ast, {
ObjectExpression(path) {
const properties = path.node.properties;
for (const prop of properties) {
if (prop.type === "ObjectProperty") {
let keyValue = "";
if (prop.key.type === "Identifier") {
keyValue = prop.key.name;
} else if (prop.key.type === "StringLiteral") {
keyValue = prop.key.value;
} else if (prop.key.type === "NumericLiteral") {
keyValue = prop.key.value.toString();
}
if (keyValue.includes(KEY_MATCH)) {
const objectCode = generator(path.node, {
compact: false,
minified: false
}).code;
const jsObject = eval(`(${objectCode})`);
for (const k of Object.keys(jsObject)) {
if (ignorePatterns.some(p => k.match(new RegExp(p)))) {
delete jsObject[k]
}
}
result = jsObject;
return;
}
}
}
}
});
return result;
} catch (error) {
console.error(`Error processing manifest`, error.message);
}
}
Файл crawler.ts
Функция main вызывается из cli.ts, динамически подгружает JS модули из папки scenarios и выполняет их последовательно в цикле, передавая в функцию crawl.Если название сценария не было указано как аргумент через ключ -s, по умолчанию подгружаются все сценарии из директории scenarios.
Функция crawl инициализирует сессию Playwright, прописывает необходимые куки и сначала выполняет customScenario, а затем обрабатывает каждый URL из urls с помощью функции visitUrls.
Спойлер: Полный код файла crawler/crawler.ts
// file crawler/crawler.ts
import { readdir, lstat } from "node:fs/promises";
import { firefox, Page } from 'playwright';
import process from 'node:process';
// логирование времени выполнения
async function runWithTiming(asyncFn) {
const start = Date.now();
try {
const result = await asyncFn();
const ms = Date.now() - start;
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const millis = ms % 1000;
let output = '';
if (minutes) output += `${minutes}m `;
if (seconds || minutes) output += `${seconds}s`;
if (!minutes && !seconds) output = `${millis}ms`;
console.log(` Execution time: ${output.trim()}`);
return result;
} catch (err) {
//console.error(" Error during execution:", err);
throw err;
}
}
// при ошибке выполнения сценария даем 5 попыток на повтор и добавляем паузу в минуту между повторами
const retrying = async (fn, maxRetries = 5) => {
const retryDelay = 60 * 1000
let tries = 0;
while (tries < maxRetries) {
try {
await fn();
if (tries) {
console.log(` Success on ${tries+1} try`);
}
break;
} catch (err) {
console.error(err);
console.error(` Error fetching URL. Sleep for a while and retrying ${tries+1}/${maxRetries}...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
tries++;
}
}
}
// автоматизация рутины посещения ссылок
async function visitUrls(page: Page, urls: string[], waitForTimeout: number, beforeEachUrl: (page: Page, url: string) => Promise<void>) {
if (!urls.length) {
return;
}
console.log('- Visiting URLs...');
let i = 1;
for (const url of urls) {
const n = `${i}`.padStart(`${urls.length}`.length, ' ');
console.log(`* ${n}/${urls.length} - ${url}`)
await runWithTiming(async () => {
await retrying(async () => {
if (beforeEachUrl) {
await beforeEachUrl(page, url);
}
const response = await page.goto(url, { waitUntil: 'load', timeout: 60000 });
if (!response.ok()) {
console.error(` [${response.status()}] ${url}`);
if (response.status() === 500) {
//throw new Error(response.status());
}
}
})
});
await page.waitForTimeout(waitForTimeout + Math.random() * 1000);
i++;
}
}
interface CrawlInput {
outputPath: string
headers: Record<string, string>
proxy?: string
urls: string[]
customScenario?: (page: Page) => Promise<void>
browserPath: string
visible?: boolean
waitForTimeout?: number
beforeEachUrl?: (page: Page, url: string) => Promise<void>
}
async function crawl({ urls, outputPath, headers, proxy, customScenario, browserPath, visible, beforeEachUrl, waitForTimeout = 9000 }: CrawlInput) {
const browser = await firefox.launch({
headless: !visible,
executablePath: browserPath,
args: ['--no-sandbox'],
proxy: proxy ? {
server: proxy
} : undefined
});
const context = await browser.newContext({
recordHar: {
path: outputPath
},
extraHTTPHeaders: {
...headers
}
});
try {
let page = await context.newPage();
if (customScenario) {
console.log('- Running custom scenario...');
await customScenario(page);
}
page = await context.newPage();
await visitUrls(page, urls, waitForTimeout, beforeEachUrl);
await context.close();
} catch (err) {
await browser.close();
throw err;
}
}
export enum CrawlRevision {
Current = 'current',
Previous = 'previous'
}
export interface MainInput {
revision: CrawlRevision
scenarios: string[]
browserPath: string
visible: boolean
maxRetries?: number
retryDelay?: number
}
export async function main({ revision, scenarios, browserPath, visible, maxRetries = 5, retryDelay = 10_000 }: MainInput) {
const cwd = process.cwd();
if (!scenarios || !scenarios.length) {
const content = await readdir(`${cwd}/crawler/scenarios`);
scenarios = [];
for (const item of content) {
// пропускаем директории
const stat = await lstat(`${cwd}/crawler/scenarios/${item}`);
if (stat.isFile()) {
scenarios.push(item);
}
}
// фиксируем порядок выполнения
scenarios.sort();
} else {
scenarios = scenarios.map(f => f.endsWith('.ts') ? f : `${f}.ts`);
}
console.log('// Scenarios ', scenarios);
for (const file of scenarios) {
const path = file.includes('/') ? file : `${cwd}/crawler/scenarios/${file}`;
console.log(`// Importing ${path}...`);
const scenarioModules = await import(path);
const scenario = scenarioModules.default;
const name = file.replace('.ts', '');
const input: CrawlInput = {
outputPath: `${cwd}/revisions/${revision}/${name}.har`,
headers: scenario.headers,
urls: scenario.urls,
customScenario: scenario.customScenario,
browserPath,
visible,
waitForTimeout: scenario.waitForTimeout
}
if (scenario.proxy) {
input.proxy = scenario.proxy;
}
if (scenario.beforeEachUrl) {
input.beforeEachUrl = scenario.beforeEachUrl;
}
console.log(`// Start execution of ${name} scenario...`);
let tries = 0;
while (tries < maxRetries) {
try {
await crawl(input);
break;
} catch (err) {
console.error(err);
console.error(` Error during execution of ${name} scenario, sleep for a while and retrying ${tries+1}/${maxRetries}...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
tries++;
}
}
}
console.log('// All scenarios executed.');
process.exit(0);
}
Запуск
❯ bun run crawler/cli.ts current
// Scenarios [ "anon.ts", "manifest.ts" ]
// Importing /home/nipaah/df-js-monitor/crawler/scenarios/anon.ts...
// Start execution of anon scenario...
- Running custom scenario...
- Visiting URLs...
* 1/2 - https://www.udemy.com
Execution time: 9s
* 2/2 - https://www.udemy.com/join/passwordless-auth/?locale=en_US&next=https%3A%2F%2Fwww.udemy.com%2F&response_type=html&action=login&mode
Execution time: 4s
// Importing /home/nipaah/df-js-monitor/crawler/scenarios/manifest.ts...
// Start execution of manifest scenario...
- Running custom scenario...
customScenario
Visiting main page to download manifest file
Downloaded manifest file https://www.udemy.com/staticx/udemy/js/webpack/entry-main-manifest.e080d99058059da9d5f0.js
Found 230 URLs to download
- Visiting URLs...
* 1/230 - https://www.udemy.com/staticx/udemy/js/webpack/common-app-css.6aba0217b0532ace1dce.js
[404] https://www.udemy.com/staticx/udemy/js/webpack/common-app-css.6aba0217b0532ace1dce.js
Execution time: 316ms
* 2/230 - https://www.udemy.com/staticx/udemy/js/webpack/activity-notifications-udlite-app.36183e97e8a8a3d357be.js
Execution time: 263ms
<SNIP>
* 230/230 - https://www.udemy.com/staticx/udemy/js/webpack/node_modules_storybook_components_dist_WithTooltip-4HIR6TLV_mjs.d0cb52092f343ab43026.js
Execution time: 459ms
// All scenarios executed.
Теперь выведем список urls, перехваченных в сценарии anon:
Спойлер: Вывод
❯ cat ./revisions/current/anon.har | jq -r '.log.entries.[].request.url' | grep -P 'udemy(cdn)?\.com' | grep '\.js'
https://frontends.udemycdn.com/design-system/icons/v18/icon-spritemap.js
https://refer.udemy.com/core.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/webpack-46f0915a09962666.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/framework-8b7974553b1447ee.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/main-f9ce038182d57638.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/pages/_app-ba0f46c98872ebce.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/840-b9e035e0e7d6f06a.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/630-63623b1d9afeff28.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/pages/lohp-bad55e859a6f5a82.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/eZg9KmI1m8-_I47wyBKaZ/_buildManifest.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/eZg9KmI1m8-_I47wyBKaZ/_ssgManifest.js
https://www.udemy.com/cdn-cgi/challenge-platform/scripts/jsd/main.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/1edf0483.9dfe51f2fe978bc8.js
https://frontends.udemycdn.com/frontends-homepage/_next/static/chunks/918.9c725618c4e6c2d9.js
https://www.udemy.com/cdn-cgi/challenge-platform/h/b/scripts/jsd/e8e65e95f26d/main.js?
https://frontends.udemycdn.com/design-system/icons/v18/icon-spritemap.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-passwordless-udlite-app.5977588272251d371a8e.js
https://www.udemy.com/staticx/udemy/js/webpack/common-desktop-udlite-app.3f63a2dff7a836293be1.js
https://www.udemy.com/staticx/udemy/js/webpack/entry-main-manifest.e080d99058059da9d5f0.js
https://www.udemy.com/staticx/udemy/js/webpack/entry-main-vendor.a88c543cec6c2406c84b.js
https://www.udemy.com/staticx/udemy/js/webpack/jsi18n-en-us.c30ac933270f04f0c158.js
https://www.udemy.com/staticx/udemy/js/webpack/entry-main.4b375aa0c675c0d33f9f.js
https://www.udemy.com/cdn-cgi/challenge-platform/scripts/jsd/main.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_auth_dist_esm_auth-form-error-field_auth-form-error-field_react_co-ba89dc.310912a2b3da97ad1277.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-passwordless-udlite-app.5977588272251d371a8e.js
https://www.udemy.com/cdn-cgi/challenge-platform/h/b/scripts/jsd/e8e65e95f26d/main.js?
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_react-header_dist_esm_events_js.b63a003c64daf2bf0cb4.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_braze_dist_esm_index_js-node_modules_udemy_browse-event-tracking_d-ee19de.222fa5bd015a3bc96633.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_activity-notifications_dist_esm_activity-notifications_mobx-store_-fb67a7.74295ed1a3638d0ad1dc.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_event-tracking_dist_esm_debugger_tracker-debugger_react-component_-91dd42.d40a6b30bd88d5486094.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_react-header_dist_esm_desktop_desktop-header_react-component_js-no-aaf7ee.376f7e75ff729d7153bf.js
https://www.udemy.com/staticx/udemy/js/webpack/common-desktop-udlite-app.3f63a2dff7a836293be1.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_amplitude_analytics-browser_lib_esm_index_js.bdcf00d02cd01ac4e130.js
Спойлер: и сценарий manifest
❯ cat ./revisions/current/manifest.har | jq -r '.log.entries.[].request.url' | grep -P 'udemy(cdn)?\.com' | grep '\.js'
https://frontends.udemycdn.com/design-system/icons/v18/icon-spritemap.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-passwordless-udlite-app.5977588272251d371a8e.js
https://www.udemy.com/staticx/udemy/js/webpack/common-desktop-udlite-app.3f63a2dff7a836293be1.js
https://www.udemy.com/staticx/udemy/js/webpack/entry-main-manifest.e080d99058059da9d5f0.js
https://www.udemy.com/staticx/udemy/js/webpack/entry-main-vendor.a88c543cec6c2406c84b.js
https://www.udemy.com/staticx/udemy/js/webpack/jsi18n-en-us.c30ac933270f04f0c158.js
https://www.udemy.com/staticx/udemy/js/webpack/entry-main.4b375aa0c675c0d33f9f.js
https://www.udemy.com/cdn-cgi/challenge-platform/scripts/jsd/main.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_auth_dist_esm_auth-form-error-field_auth-form-error-field_react_co-ba89dc.310912a2b3da97ad1277.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-passwordless-udlite-app.5977588272251d371a8e.js
https://www.udemy.com/cdn-cgi/challenge-platform/h/b/scripts/jsd/e8e65e95f26d/main.js?
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_react-header_dist_esm_events_js.b63a003c64daf2bf0cb4.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_braze_dist_esm_index_js-node_modules_udemy_browse-event-tracking_d-ee19de.222fa5bd015a3bc96633.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_activity-notifications_dist_esm_activity-notifications_mobx-store_-fb67a7.74295ed1a3638d0ad1dc.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_event-tracking_dist_esm_debugger_tracker-debugger_react-component_-91dd42.d40a6b30bd88d5486094.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_udemy_react-header_dist_esm_desktop_desktop-header_react-component_js-no-aaf7ee.376f7e75ff729d7153bf.js
https://www.udemy.com/staticx/udemy/js/webpack/common-desktop-udlite-app.3f63a2dff7a836293be1.js
https://www.udemy.com/staticx/udemy/js/webpack/vendors-node_modules_amplitude_analytics-browser_lib_esm_index_js.bdcf00d02cd01ac4e130.js
https://www.udemy.com/staticx/udemy/js/webpack/common-app-css.6aba0217b0532ace1dce.js
https://www.udemy.com/staticx/udemy/js/webpack/activity-notifications-udlite-app.36183e97e8a8a3d357be.js
https://www.udemy.com/staticx/udemy/js/webpack/assessment-not-available-udlite-app.3cf544de4da61135d048.js
https://www.udemy.com/staticx/udemy/js/webpack/assessments-udlite-app.386a208bb0731b441d38.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-passwordless-udlite-app.5977588272251d371a8e.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-turnstile-udlite-app.963e887173bdf3fa17df.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-two-factor-udlite-app.7ca6e5cfa39fc9315057.js
https://www.udemy.com/staticx/udemy/js/webpack/auth-udlite-app.9f14426d117d616cf739.js
https://www.udemy.com/staticx/udemy/js/webpack/browse-udlite-app.5fe8e6346b209235cbd1.js
https://www.udemy.com/staticx/udemy/js/webpack/cart-pages-cart-udlite-app.272f5e99a61d6d1e587e.js
https://www.udemy.com/staticx/udemy/js/webpack/cart-pages-success-udlite-app.ee228570f62e2bfd34e9.js
https://www.udemy.com/staticx/udemy/js/webpack/category-free-udlite-app.a343d39d3771fc829078.js
https://www.udemy.com/staticx/udemy/js/webpack/discovery-common.3f7bedf84fdf9173e4a6.js
https://www.udemy.com/staticx/udemy/js/webpack/category-udlite-app.085421df27485bd7431d.js
https://www.udemy.com/staticx/udemy/js/webpack/checkout-apps-payment-method-management-udlite-app.5903dfffe8a1237a3f03.js
https://www.udemy.com/staticx/udemy/js/webpack/checkout-marketplace-udlite-app.a53b19a6096aa1bd8785.js
https://www.udemy.com/staticx/udemy/js/webpack/checkout-teamplan-checkout-udlite-app.c23f27c26cf6e10986fb.js
https://www.udemy.com/staticx/udemy/js/webpack/checkout-teamplan-resubscribe-udlite-app.850e5a8d0880e887a8f7.js
https://www.udemy.com/staticx/udemy/js/webpack/checkout-teamplan-upsell-udlite-app.7d275606ab73422e25b2.js
https://www.udemy.com/staticx/udemy/js/webpack/coaching-join-booking-udlite-app.7af66ed427d9727a202d.js
https://www.udemy.com/staticx/udemy/js/webpack/coaching-udlite-app.950a67de42c8b7361637.js
https://www.udemy.com/staticx/udemy/js/webpack/collections-udlite-app.7e3c6629fea599c46bdb.js
https://www.udemy.com/staticx/udemy/js/webpack/common-desktop-udlite-app.3f63a2dff7a836293be1.js
https://www.udemy.com/staticx/udemy/js/webpack/common-mobile-udlite-app.3145a8df36bcd92a9bf4.js
https://www.udemy.com/staticx/udemy/js/webpack/common-ufb-desktop-udlite-app.360ed41f4c725e6e2606.js
https://www.udemy.com/staticx/udemy/js/webpack/common-ufb-mobile-udlite-app.68b44b0daad4f9038b6b.js
https://www.udemy.com/staticx/udemy/js/webpack/course-auto-enroll-udlite-app.aef498b5868d9858f6e7.js
https://www.udemy.com/staticx/udemy/js/webpack/course-certificate-udlite-app.5cc1bafb13bf90314fbd.js
https://www.udemy.com/staticx/udemy/js/webpack/course-extract-udlite-app.0bb2128618b104871040.js
https://www.udemy.com/staticx/udemy/js/webpack/course-landing-page-free-udlite-app.db35667c2a1fd5ba7a01.js
https://www.udemy.com/staticx/udemy/js/webpack/course-landing-page-private-udlite-app.76f233cbfb0a6a17f245.js
https://www.udemy.com/staticx/udemy/js/webpack/course-landing-page-udlite-app.0c38832c8480c9771abf.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-announcements-udlite-app.9000a834ec9c8f704d53.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-coding-exercise-udlite-app.41d678ea3bfa60db4546.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-create-udlite-app.6eface683e23c44c3781.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-practice-test-udlite-app.c47faf8582dcdd8cbfa8.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-practice-udlite-app.4149ac68c0c0c1968b06.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-role-play-udlite-app.fbdc8d7b2329d15e801c.js
https://www.udemy.com/staticx/udemy/js/webpack/course-manage-v2-udlite-app.56aa06288c0b4c7a11ec.js
https://www.udemy.com/staticx/udemy/js/webpack/course-preview-udlite-app.7d5ebb1e9f0168f86eb4.js
https://www.udemy.com/staticx/udemy/js/webpack/course-qa-fullpage-udlite-app.c4900ab9e5b46cefe2cb.js
https://www.udemy.com/staticx/udemy/js/webpack/course-taking-udlite-app.53786adfbe7981236460.js
https://www.udemy.com/staticx/udemy/js/webpack/credit-history-udlite-app.e16b5b88442b31409c89.js
https://www.udemy.com/staticx/udemy/js/webpack/documentation-udlite-app.8b06042c8f4003a8ef3c.js
https://www.udemy.com/staticx/udemy/js/webpack/examples-react-codelab-udlite-app.59cb332ecb0eb7fa0ec4.js
https://www.udemy.com/staticx/udemy/js/webpack/examples-react-with-typescript-udlite-app.87e2ffe88842b3a0513d.js
https://www.udemy.com/staticx/udemy/js/webpack/examples-udlite-app.d03b2d7d9c8168733323.js
https://www.udemy.com/staticx/udemy/js/webpack/experimental-no-adaptive-assessment-udlite-app.0dab47386ddc3b1f39f0.js
https://www.udemy.com/staticx/udemy/js/webpack/file-upload-udlite-app.9024342c050e5e018eb6.js
https://www.udemy.com/staticx/udemy/js/webpack/forgot-password-udlite-app.3f6d3841f218b7e98931.js
https://www.udemy.com/staticx/udemy/js/webpack/form-fields-udlite-app.7733b0fbbdf535c08e01.js
https://www.udemy.com/staticx/udemy/js/webpack/get-mobile-app-udlite-app.371022785c40aefcd18d.js
https://www.udemy.com/staticx/udemy/js/webpack/gift-udlite-app.97ce7798a335c13d4017.js
https://www.udemy.com/staticx/udemy/js/webpack/home-udlite-app.d5e2ba32d7198de0652b.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-onboarding-udlite-app.de2d3c9039eb89aa73ea.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-verification-udlite-app.3dd4b37d3dc075e27f8b.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-coaching-udlite-app.98d6bc60e17bbb769b2a.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-create-course-bundle-udlite-app.59b85441771f152b413d.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-header-udlite-app.93d314edf0fb278ffb54.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-multiple-coupons-creation-udlite-app.1bb5d7303ee05300cf7d.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-qrp-reactivate-udlite-app.65f0e32585da135fc7c1.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-side-nav-udlite-app.b59f8e9da352daa9fc83.js
https://www.udemy.com/staticx/udemy/js/webpack/vendor-moment.21fb0c76cf912f74172d.js
https://www.udemy.com/staticx/udemy/js/webpack/instructor-udlite-app.04e191e81614f313c55d.js
https://www.udemy.com/staticx/udemy/js/webpack/invite-udlite-app.12f46a22ffba98fa673d.js
https://www.udemy.com/staticx/udemy/js/webpack/lab-manage-udlite-app.320b03888a8064054f55.js
https://www.udemy.com/staticx/udemy/js/webpack/lab-reference-code-udlite-app.3fa9a4afb9e6120819b9.js
https://www.udemy.com/staticx/udemy/js/webpack/lab-taking-udlite-app.6103aa77c650ae0c8226.js
https://www.udemy.com/staticx/udemy/js/webpack/lab-workspace-udlite-app.6150504e0556af4b5feb.js
https://www.udemy.com/staticx/udemy/js/webpack/labs-landing-udlite-app.1889f2b0f3be12381e6c.js
https://www.udemy.com/staticx/udemy/js/webpack/labs-loading-udlite-app.c8cdc738a98a602777bb.js
https://www.udemy.com/staticx/udemy/js/webpack/learner-activity-v1-udlite-app.64320db184336d9ee631.js
https://www.udemy.com/staticx/udemy/js/webpack/learning-assistant-udlite-app.f9beb5d5987cefdc38d1.js
https://www.udemy.com/staticx/udemy/js/webpack/learning-path-udlite-app.34d237c10622e774813a.js
https://www.udemy.com/staticx/udemy/js/webpack/lecture-landing-page-desktop-udlite-app.c1627530ecfea672d37b.js
https://www.udemy.com/staticx/udemy/js/webpack/lecture-landing-page-mobile-udlite-app.b23dee6bcadd9926c10f.js
https://www.udemy.com/staticx/udemy/js/webpack/lecture-preview-udlite-app.8e338a7348f506700bc4.js
https://www.udemy.com/staticx/udemy/js/webpack/lihp-udlite-app.606f7a8d7e7f9ba2133e.js
https://www.udemy.com/staticx/udemy/js/webpack/lohp-udlite-app.9713898c11d83d90cb1a.js
https://www.udemy.com/staticx/udemy/js/webpack/messaging-udlite-app.a21fb55c8cf9652fa2f8.js
https://www.udemy.com/staticx/udemy/js/webpack/mobile-app-download-udlite-app.d5b31d99b67f5747f7a8.js
https://www.udemy.com/staticx/udemy/js/webpack/mobile-app-variables-udlite-app.26784dc0220f75139100.js
https://www.udemy.com/staticx/udemy/js/webpack/my-courses-v3-udlite-app.d647e1b5a0f02db23911.js
https://www.udemy.com/staticx/udemy/js/webpack/open-badges-udlite-app.dc64a8ed57282a54ef06.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-admin-landing-experience-udlite-app.e0778d7e0c64b933651a.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-common-team-plan-payment-redirect-udlite-app.64ceaa301f67dfe37d8e.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-common-team-plan-renewal-notice-udlite-app.c6cbb3e52364b8dd2e15.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-course-not-available-udlite-app.3c4a8eac0d21dcb3a2b7.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-global-login-udlite-app.755d2f192ee53caee2e3.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-growth-team-plan-sign-up-udlite-app.439efa0b3959654d941f.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-home-udlite-app.84251084679ae6881fe4.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-insights-export-reports-udlite-app.1431cb00d63faeacab4d.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-insights-udlite-app.9a792397041c869368a3.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-invitation-verification-udlite-app.c8e0297967f770b7e190.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-login-udlite-app.73b73c98dcccc126930b.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-assigned-udlite-app.93de36b1e75445c18b4d.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-courses-udlite-app.598b3ab2dc3bfcffed6d.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-approved-email-domains-udlite-app.405873b78009ea23eb01.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-custom-error-message-udlite-app.05d83849edada7d8c97a.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-customize-appearance-udlite-app.d302d3ac2697f2b1a784.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-email-reminders-udlite-app.f6fcb62495e55bbfa689.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-genai-features-udlite-app.7387ff3c4a6b00bffbb1.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-integrations-udlite-app.6b4314dfe54f2a57686f.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-lms-integration-udlite-app.551f81d9da4ac82d1cdf.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-mcp-udlite-app.6072e75c38f5bff2f5dd.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-multi-factor-authentication-udlite-app.d6c2a93c589b49c63d72.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-notification-preferences-udlite-app.7e05c69c2f24f9863f94.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-provisioning-scim-udlite-app.7dd260920fecb1b0de87.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-single-sign-on-self-serve-udlite-app.c704250ef8a5c923c27b.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-settings-sso-cert-utility-udlite-app.d5295eba88a5d9484eab.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-manage-users-udlite-app.87f6d2d27ecd88f674d9.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-merge-consent-udlite-app.42882a6415b782bd38ed.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-merge-frozen-udlite-app.2eb126a00ac2d7dc4ce6.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-onboarding-pro-udlite-app.b7aa11428e20d1809c86.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-resources-udlite-app.e3cea48b7fcc4a5076a1.js
https://www.udemy.com/staticx/udemy/js/webpack/organization-team-plan-billing-udlite-app.9e7800e1a0f66ad5304e.js
https://www.udemy.com/staticx/udemy/js/webpack/personal-plan-home-udlite-app.1e8a877b0cc430a63d96.js
https://www.udemy.com/staticx/udemy/js/webpack/personalize-udlite-app.87fdeb34cd3b4cef6fc4.js
https://www.udemy.com/staticx/udemy/js/webpack/prepaid-code-udlite-app.a28d7ace0e8a709d35ae.js
https://www.udemy.com/staticx/udemy/js/webpack/purchase-history-udlite-app.794cb08969e795b8c7bf.js
https://www.udemy.com/staticx/udemy/js/webpack/report-abuse-udlite-app.eceb2aaacfea661583fc.js
https://www.udemy.com/staticx/udemy/js/webpack/revenue-report-udlite-app.73f4425b5c16b81c799d.js
https://www.udemy.com/staticx/udemy/js/webpack/search-udlite-app.bbe4070feac2cd1a2b21.js
https://www.udemy.com/staticx/udemy/js/webpack/shaka-video-player-tester-udlite-app.eed3ca4f3bb4e18de509.js
https://www.udemy.com/staticx/udemy/js/webpack/student-refund-udlite-app.4aa5ad5d5112e5f85110.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-browse-pages-subscription-logged-in-home-udlite-app.0a48ce02dd44481292d2.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-checkout-v2-udlite-app.16dbb7fe2e5a60551079.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-checkout-header-udlite-app.368481fd5e80998f65ea.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-checkout-pages-checkout-success-udlite-app.c719fe59565f3b31675a.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-checkout-udlite-app.57ba2e67807ab1eb07f3.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-management-settings-udlite-app.7e03dc0d137237bf5c1c.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-plan-selection-udlite-app.cf6947169458bdb0635a.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-required-udlite-app.8b5b02be0c49a5b71cd7.js
https://www.udemy.com/staticx/udemy/js/webpack/subscription-upgrade-checkout-udlite-app.65d4bb93e6f1fce392b4.js
https://www.udemy.com/staticx/udemy/js/webpack/support-system-check-udlite-app.30dbe368083ab499b555.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-autocomplete-admin-udlite-app.dc3947f1ce9c38a82763.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-change-gateway-payment-transaction-admin-udlite-app.5a3d34e71adecac13090.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-change-payment-transaction-admin-udlite-app.3efa14afd6f9060401c2.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-customer-data-platform-admin-attribute-udlite-app.9d7dcf9832b24800fc67.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-customer-data-platform-admin-graph-udlite-app.05310419e5296786a89e.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-customer-data-platform-admin-s3-catalog-udlite-app.97956783adb8058af62a.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-delete-abn-admin-udlite-app.6956149dd9ea935be564.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-discovery-cache-admin-udlite-app.199ada704dbfe5a7897d.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-discovery-context-admin-udlite-app.0077e04966c1630f169f.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-discovery-unit-form-admin-udlite-app.7ff8c0f25da289ed23b3.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-es-tooling-admin-udlite-app.bf589825a0909d11d945.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experiment-form-admin-udlite-app.29f0bb3ca789a5a899a9.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-abn-experiment-management-udlite-app.91a48def076bb52ecfb1.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-attribute-collector-udlite-app.9e9cfee39f185bbea740.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-business-domain-udlite-app.7326db3fb1e66c12cf9a.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-configuration-context-udlite-app.982e7f48aa70fdceb90c.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-configuration-domain-udlite-app.2fba1bb56048e45ecbe7.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-dashboard-udlite-app.ba5066e8793fae11344a.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-experiment-group-udlite-app.b276d70669eb7d0cb385.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-experiment-management-udlite-app.3b5b7e5cac17c1126a1d.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-feature-udlite-app.31c40beea08fc9238be5.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-isolation-group-udlite-app.23e5e733e9b21ae085c2.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-ledger-udlite-app.b55322af7544e1281223.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-plan-udlite-app.77a50c79360fe44a6930.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-experimentation-platform-admin-previews-udlite-app.11caa9e16d465a544405.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-instructor-course-retirement-notification-admin-udlite-app.9c9cff27f5112a72b141.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-jsoneditor-admin-udlite-app.5c923f97c64839318f79.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-labs-create-new-lab-admin-udlite-app.42df84486afdf80a5000.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-marketing-tools-admin-udlite-app.693ca90d4a5992d730d8.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-measure-competence-admin-udlite-app.9bc0fc54c39d143b6332.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-collections-admin-udlite-app.15565fa75deada55c7aa.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-insights-admin-data-export-reports-udlite-app.420d6f86b9353184afc0.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-insights-admin-insights-udlite-app.1a46fc752893fdf6790d.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-merge-admin-create-merge-request-udlite-app.2606842a962f42e3e9f3.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-merge-admin-merge-request-udlite-app.1d5ef01c18654c9e4dd4.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-new-owner-widget-admin-udlite-app.811f6614112d3fa39c44.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-overview-admin-admin-overview-udlite-app.6e12438bbb35dc86f2f1.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-owner-widget-admin-udlite-app.428468ec380ad45cd12c.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-subscription-admin-udlite-app.4cad0c710e0f52636794.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-organization-support-admin-udlite-app.4e5d8e4ef05ee5971fb4.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-payment-method-admin-udlite-app.aa6ddce0b5d5a781851c.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-payment-method-config-admin-udlite-app.52bbf22477b3ddf39f21.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-payment-story-admin-udlite-app.d77c05e59abd13a9de83.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-payouts-admin-udlite-app.ca428b34e86837fa3930.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-prepaid-code-admin-prepaid-code-management-udlite-app.fba197d0e46309ad9bb1.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-prepaid-code-admin-prepaid-code-request-create-udlite-app.481afe5de1e1f8a79f15.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-prepaid-code-admin-prepaid-code-request-management-udlite-app.e6d6209cbf57e6653b9a.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-pricing-admin-coupon-management-udlite-app.6b051ce6570985fc3b8e.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-quality-review-admin-udlite-app.9b3c01f1e8615519f802.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-refund-admin-udlite-app.b1d9027b613410cd7865.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-refund-gateway-transaction-admin-udlite-app.7d96f0f14c46d665c7a8.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-refund-transaction-admin-udlite-app.8168890322e60d45b228.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-sherlock-admin-udlite-app.41ee683328e9e3836640.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-structured-data-search-admin-udlite-app.543174f9aa3884563959.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-subscription-management-admin-udlite-app.3e0fd9a0e4776763d6e7.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-support-admin-udlite-app.424188ae3fafc9ff0954.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-task-management-admin-udlite-app.c74b43f4d72cbb7d3806.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-user-admin-udlite-app.ad15aa40e41f93979ff6.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-user-test-video-admin-udlite-app.976ed85c624bdc258922.js
https://www.udemy.com/staticx/udemy/js/webpack/tapen-video-player-admin-udlite-app.8f97566500e4f43d661e.js
https://www.udemy.com/staticx/udemy/js/webpack/teach-page-udlite-app.8ece14ca6e7e13e7ceaa.js
https://www.udemy.com/staticx/udemy/js/webpack/teaching-courses-test-video-udlite-app.857970d829cbd81f8245.js
https://www.udemy.com/staticx/udemy/js/webpack/terms-bar-udlite-app.20e10c0062ca9a2dc454.js
https://www.udemy.com/staticx/udemy/js/webpack/topic-udlite-app.39e7484746447778143a.js
https://www.udemy.com/staticx/udemy/js/webpack/user-manage-ajax-modal-udlite-app.a0ff1677daf84acf3190.js
https://www.udemy.com/staticx/udemy/js/webpack/user-manage-udlite-app.7e4f75d7a646212f6bbc.js
https://www.udemy.com/staticx/udemy/js/webpack/user-profile-instructor-udlite-app.2119364dea5d6b8b529c.js
https://www.udemy.com/staticx/udemy/js/webpack/user-profile-udlite-app.1e1a25440468d08e6067.js
https://www.udemy.com/staticx/udemy/js/webpack/versioned-image-upload-with-preview-udlite-app.45d398cc7496798224c3.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_amplitude_engagement-browser_index_esm_js.49b8cdad9f0c8c75d03a.js
https://www.udemy.com/staticx/udemy/js/webpack/braze.132680f27e8e69ab2079.js
https://www.udemy.com/staticx/udemy/js/webpack/src_udemy_js_pendo_bootstrap_ts.473612c26800e515eb0b.js
https://www.udemy.com/staticx/udemy/js/webpack/src_udemy_js_my-courses-v3_measure-competence_assessment-card-context-menu-lazy_react-component_js.bbeb00a552ed5f08ed79.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_udemy_mathjax_dist_esm_load-mathjax_js.5573126e1da665eea6a0.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_udemy_react-rich-text-editor_dist_esm_rich-text-editor_ud-prosemirror_index_js.0c53f76addd33edc02ed.js
https://www.udemy.com/staticx/udemy/js/webpack/zxcvbn-common.5f88745a8de6d5c2cc60.js
https://www.udemy.com/staticx/udemy/js/webpack/zxcvbn-en.d04c0778a2aa33873fa7.js
https://www.udemy.com/staticx/udemy/js/webpack/zxcvbn-de.01c17d8c425f156368b8.js
https://www.udemy.com/staticx/udemy/js/webpack/zxcvbn-fr.c222939ef20afac11d1b.js
https://www.udemy.com/staticx/udemy/js/webpack/zxcvbn-es.4f6310bef9789f1fdc67.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_udemy_react-discovery-units_dist_esm_professional-skills-unit_subcategory-conten-2b559b.9e7956b77ffa97b348a7.js
https://www.udemy.com/staticx/udemy/js/webpack/create-ufb-context-menu.0d9af80fc2ab45672303.js
https://www.udemy.com/staticx/udemy/js/webpack/course-landing-page-lazy.4b0ea6eaf46816a37bb6.js
https://www.udemy.com/staticx/udemy/js/webpack/course-landing-page-lazy-course-context-menu.2afb5df17083625547f6.js
https://www.udemy.com/staticx/udemy/js/webpack/marketplace-social-share.61ebb8685a28827eecdc.js
https://www.udemy.com/staticx/udemy/js/webpack/shaka-video-player.a951c6c1ddd629b52e30.js
https://www.udemy.com/staticx/udemy/js/webpack/video-mashup-asset.1fa9fa5947e271b57fb1.js
https://www.udemy.com/staticx/udemy/js/webpack/react-syntax-highlighter/lowlight-import.588ea811ae57da2cc0b3.js
https://www.udemy.com/staticx/udemy/js/webpack/throw-error.e534a479f9d99b9894cb.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_udemy_react-student-retention_node_modules_progressbar_js_dist_progressbar_js.33c959eeee42c18fff40.js
https://www.udemy.com/staticx/udemy/js/webpack/search-unavailable.2dd641dfecaa6de13110.js
https://www.udemy.com/staticx/udemy/js/webpack/search-no-results.95e687be68791ffb0fa6.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_storybook_blocks_dist_Color-6VNJS4EI_mjs.81308e96f40e9436df67.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_storybook_components_dist_syntaxhighlighter-NMPM6SWI_mjs.c44add36fd696ac4c558.js
https://www.udemy.com/staticx/udemy/js/webpack/node_modules_storybook_components_dist_WithTooltip-4HIR6TLV_mjs.d0cb52092f343ab43026.js
Заключение
Таким образом, с помощью анти-детект браузера, фреймворка Playwright и парсинга манифест-файла из Webpack мы собрали максимально возможное количество JS-файлов с нашего таргета за Cloudflare. И хотя может показаться, что можно обойтись только манифест-файлом, полагаться на него не стоит — в зависимости от настроек сборки некоторые файлы могут отсутствовать, а на многих таргетах такого файла может не быть вовсе. Чанки могут быть описаны и без манифеста внутри бандла, но метод их извлечения будет точно таким же; изменится только алгоритм поиска в бандле и формирование итогового списка URL.В следующей части рассмотрим работу с HAR-файлами для извлечения из них JS, их форматирование, извлечение source maps и подготовку к работе с JS extractors.
LIMB