Автоматизація security-перевірок для AQA/SDET
Зміст
Попередні чотирнадцять глав розділу відповідали на питання «що шукати». Ця — на питання «як зробити, щоб про це памʼятав пайплайн, а не людина». AQA тут не стає пентестером: він бере інваріанти, які вже вміє формулювати словами — чужа роль не бачить чужого рахунку, сесійна кука має бути HttpOnly, адмінський відповідає 403 анонімові — і перетворює їх на , файли й кроки (pipeline).
Саме тут живе найдорожча пастка всього розділу — зелений крок, який нічого не означає.
Куди в пайплайні це ставлять
Типова хиба планування — уявити «процес безпеки поруч» із наявним пайплайном. Канонічна модель робить навпаки: «we can add more steps in Dev pipelines» — це додані кроки в тому самому пайплайні, а мета сформульована як швидкість: «detect security issues (by design or application vulnerability) as fast as possible».
Порядок стадій у гайдлайні OWASP і є відповіддю на «куди»: (нульовим кроком, до коду) → pre-commit із керуванням секретами й лінтингом → сканування вразливостей (, , IAST, , інфраструктура, ) → аудит відповідності. Шпаргалка OWASP про безпеку CI/CD формулює те саме вимогою — вбудувати ці перевірки в пайплайн, а перед у прод лишити ручний апрув. Там-таки стоїть застереження, з якого росте решта цієї глави: «one should never blindly rely on default vendor settings».
Рівня в ці джерела security-перевіркам не призначають — поняття піраміди в них не вживається взагалі. Місце визначає стадія пайплайна й те, що перевірці потрібно на вході: сканеру коду — репозиторій, DAST — запущений застосунок за доступною адресою, асерту в тесті — відповідь сервера. Самі стадії й інструменти докладніше — «Інструменти й процес security-тестування»; тут вони потрібні лише як координати, куди лягають наступні розділи глави.
Security-асерти: предмет перевірки — відповідь, а не екран
Автотест не бачить «безпеки». Він бачить конкретні поля: код відповіді, заголовки, кукі, TLS-дані й стан на сервері після дії. (security assertion) — не окремий жанр тесту, а перевірка, предметом якої стала властивість безпеки відповіді.
Змішаний сценарій «дію робимо в браузері, перевіряємо запитом» дока Playwright санкціонує прямо: виклики до HTTP-API з UI-тесту потрібні, щоб підготувати стан сервера або перевірити постумови після дії в браузері. Це дає те, чого UI не дає ніколи: відповідь на питання, чи справді сервер . Те саме в OWASP: «Relying on UI hiding is insufficient; the API layer must enforce the check».
Три канали й межа кожного
- Код відповіді ≠ «успішність».
ok()означає буквально «status in the range 200-299», тож403і404дадуть однаковеfalse— а різниця між ними і є предметом перевірки доступу. Потрібенstatus(). - Заголовки мають дві форми, і одна з них бреше. Масивна підписана в доці окремо: «Headers with multiple entries, such as Set-Cookie, appear in the array multiple times.» Перевірка прапорців кукі по обʼєкту заголовків мовчки побачить одне значення на імʼя.
- TLS-дані доступні, але порожнеча тут виглядає як успіх: для не-HTTPS повертається
null, а для редиректу — дані останнього запиту ланцюга. «Сертифікат ок» і «сертифіката не дивилися» розрізняє тільки перевірка самого факту наявності даних.
const res = await request.get('/api/invoices/42', {
headers: { Authorization: `Bearer ${tokenOfAnotherUser}` },
});
// не ok(): 403 і 404 обидва дали б false
expect(res.status()).toBe(403);
// прапорці — тільки з масиву: Set-Cookie у відповіді може бути кілька
const setCookie = res
.headersArray()
.filter((h) => h.name.toLowerCase() === 'set-cookie');
expect(setCookie.every((h) => /HttpOnly/i.test(h.value))).toBe(true);
У WebDriver робота з кукі вбудована: конкретна кука читається за іменем, усі — одним викликом, привʼязаним до живого контексту, а набір полів обмежений специфікацією. Окрема пастка — момент асерту: дока Playwright підписує очікування коментарем просто в прикладі логіну — «Sometimes login flow sets cookies in the process of several redirects», тож треба дочекатися кінцевого URL, а фіксованої паузи дока не пропонує взагалі.
Що саме перевіряти в заголовках — глава про заголовки безпеки, CSP і клікджекінг; прапорці як предмет — «Сесії та токени» і «CSRF і SameSite»; механіка самої кукі — розділ 03.
Негативні сценарії зі шпаргалки REST Security
Шпаргалка OWASP дає перелік, з якого очікування тестів виводяться майже механічно:
- Коди відмови різні за змістом:
401— «Wrong or no authentication ID/password provided»;403— автентифікація пройшла, але прав на ресурс немає. Поруч:405на метод поза списком дозволених,429на перевищення частоти,413на завеликий пейлоад,406/415на неочікуваний тип вмісту. - Токен: заборона (
alg: none), заборона обирати алгоритм перевірки за заголовком самого токена, мінімум клеймів —iss,aud,exp,nbf. Найцікавіший кейс — «запит токеном після логауту»: джерело описує розрив між життям токена й станом сесії, завершеної явним виходом або . - Порядок кроків процесу: чи можна викликати ендпоінти поза послідовністю, чи валідує кожен поточний стан, чи перевикористовуються токени між кроками. Звичайні перевірки прав цього не ловлять, бо кожен ендпоінт авторизується окремо.
- Не тільки код, а й текст: чи внутрішні деталі в тілі — знахідка навіть при «правильному» статусі.
Ще один прийом задарма: офіційний приклад зміни запиту в Playwright — це видалення заголовка перед route.continue(). Так перевіряють, що доступ тримає сервер, а не клієнт, який слухняно надіслав службовий заголовок.
Матриця авторизації як код
Причина, чому матрицю виносять у файл, названа прямо: «most of the problems with authorizations occur because features are added/modified in updated releases without determining their effect on the application's authorizations». Авторизація ламається не на старті, а в наступних релізах — звідси й рекомендація автоматизувати оцінювання прав і ганяти тест на кожен реліз. Надбудова називає це проблемою «другого дня».
(authorization matrix) має два виміри й необовʼязковий третій: фіча, логічна роль і — за потреби — дані. Роль під час прогону називають (Point Of View, POV).
Файл-півот обслуговує двох читачів одразу: його легко обробити програмою і легко прочитати людині. Тести не переписують правил, а беруть файл входом; падіння читабельне, бо називає саму комбінацію — сервіс DeleteMessage, точка зору ANONYMOUS, отриманий 200 замість очікуваних 403. Обидві половини перевірки беруться з того самого файлу: дозволений доступ і заборонений рівноправні. Блоків три: roles, services (з ролями, яким дозволено виклик) і services-testing (пейлоад для сервісів, що беруть дані не лише з URL); значення у фігурних дужках {} — плейсхолдери, які тест підставляє під час прогону.
Формат: дві шпаргалки OWASP кажуть протилежне
Тут канон сам із собою не збігається, і знати про це корисніше, ніж вибрати сторону навмання.
- Шпаргалка Authorization Testing Automation: «the two dimensions of each authorization should be listed in a spreadsheet that is called an authorization matrix» — і приклад там тримається у файлі XML.
- Шпаргалка Authorization Regression Testing робить той самий вибір навпаки й прямо: «Store this matrix in a machine-readable format (e.g., JSON, YAML, or structured test fixtures) rather than a spreadsheet. This allows testing frameworks to dynamically generate test cases, reducing manual maintenance as the authorization policy evolves.»
Обидва джерела чинні й канонічні, причому другий сам позиціює себе як надбудову над першим — він «extends that foundation by focusing on the continuous regression dimension». Практично: питання на ревʼю «чому таблиця, а не YAML» закривається посиланням на конкретну шпаргалку, а не суперечкою про смаки.
Набір і патерни
Один тест-кейс на точку зору — тоді результати профілюються за рівнем доступу. звичайні (pytest, Jest, JUnit), і тести йдуть у тому самому CI-пайплайні, що функціональні; набір виділяють тегом або окремим скриптом, щоб ганяти локально, а перемикання ролі роблять підміною токена в заголовку, а не повним логіном.
import matrix from './authorization-matrix.json';
for (const service of matrix.services) {
for (const pov of matrix.roles) {
const expected = service.allowedRoles.includes(pov.name) ? 200 : 403;
test(`${service.name} · POV ${pov.name}`, async ({ request }) => {
const res = await request.fetch(service.url, {
method: service.method,
headers: pov.headers, // підміна токена, а не повторний логін
data: service.payload,
});
expect(
res.status(),
`${service.name} з POV ${pov.name}: очікували ${expected}`,
).toBe(expected);
});
}
}
| патерн | рецепт | критерій |
|---|---|---|
| Горизонтальна ескалація (IDOR) | користувач B тієї ж ролі читає, змінює й видаляє ресурс користувача A | «must return a 403 Forbidden or 404 Not Found ... never a 200 OK» |
| Вертикальна ескалація | усі неадміністративні ролі, включно з неавтентифікованою, стукають в адмінські ендпоінти | перевірку робить API; приховування в UI не рахується |
| Ізоляція тенантів | широкий читальний запит від користувача тенанта Beta по даних тенанта Alpha | «Even a single leaked record identifier constitutes a critical failure.» |
Окремий важіль — контракт як джерело істини: якщо специфікація вимагає для ендпоінта read:invoices, фреймворк має сам перевірити, що токен без нього дістає 401 або 403. І перевірка потрібна на кожному ендпоінті, бо кожен виклик REST самостійний.
Межа теми. Матриця як комбінацій — які клітинки бувають, які коди очікувані, чому дефолт незаповненої клітинки «заборонено» — живе в главі «Авторизація в API»; атакувальний кут і означення ескалацій — «Авторизація та контроль доступу».
ZAP у пайплайні: baseline проти повного скану
( Baseline scan) — не окремий продукт, а скрипт із Docker- ZAP. Він пускає павука по цілі (типово хвилину) і чекає завершення . Чому його не бояться ставити в кожен прогін, сказано прямо: скрипт «doesn't perform any actual 'attacks'» і триває кілька хвилин щонайбільше, тож придатний для CI/CD «even against production sites».
Повний скан — інша тварина, і дока сама називає його клас: це запуск «ZAP Full Scan to perform Dynamic Application Security Testing (DAST)».
| baseline | повний скан | |
|---|---|---|
| павук | типово 1 хвилина | без ліміту часу |
| активний скан | немає | повний активний скан |
| дозвіл на ціль | придатний навіть проти продакшну | «WARNING this action will perform attacks on the target website» |
| побічні ефекти | немає атак | сабмітить форми — «Contact us» і коментарі полетять реальними повідомленнями |
Попередити треба не лише власника застосунку: дока окремо каже звірити прогін із хостингом і CDN. Третій пакетний скан — активний прогін по API, визначених OpenAPI або GraphQL.
Дві деталі оточення зʼїдають години. Найлегший образ bare пакетних сканів не містить. І окремо: «By default Docker does not allow apps running in one container to access an app running in another container» — сканер, який «нічого не знайшов», міг просто не достукатися до цілі. GitHub-дії при цьому не є окремим сканером: вони обгортають ті самі пакетні скани, ведуть один issue з алертами, кладуть звіт артефактом і працюють на штатному токені дії.
Перше, що робиться після першого прогону, — конфіг-файл: він генерується параметром -g, а далі «Edit the file and change WARN to IGNORE to ignore rule or FAIL to fail as required», тобто рішення ухвалюється по кожному правилу. Відомий борг виносять не в IGNORE, а в progress-файл: там знахідки позначаються як IN-PROGRESS, тож у звіті видно саме нові.
Сканування коду в репозиторії
(code scanning) — функція GitHub, якою аналізують код у репозиторії, щоб знайти вразливості безпеки й помилки кодування; знайдені проблеми показуються в самому репозиторії. Механізм закриває дві задачі одразу — наявний борг і недопуск нового, — а запускається за розкладом або на подію на кшталт пушу. Одиниця результату — алерт, і його цикл замикається сам: виправили код, який його спричинив, — алерт закривається.
Це не один аналізатор, а місце для аналізаторів: можна взяти CodeQL, можна сторонній інструмент, бо сумісність тримається на форматі — (Static Analysis Results Interchange Format); сам аналізатор при цьому може крутитися у зовнішньому CI. Дві межі видно в бюджеті: скан витрачає хвилини GitHub Actions, а для приватних репозиторіїв потрібна ліцензія Code Security. І одна межа, яку видно, лише якщо про неї знати: порожній звіт має два пояснення — чистий код і невідпрацьований інструмент. Під це є окрема сторінка стану інструментів.
Гейти: коли знахідка валить білд
Сканер знаходить — (CI/CD gating) вирішує. І вирішує він не про знахідку, а про код виходу. Сенс конструкції джерело формулює без помʼякшень: «The value of an authorization regression suite is only realized if it prevents vulnerable code from merging.» Механіка та сама, що в будь-якої перевірки: набір має бути required check, і якщо тест авторизації падає, не мерджиться. Без обовʼязкової перевірки падіння не заважає — і гейтом не є.
Місце перше: за замовчуванням знахідка — не FAIL, а WARN. Дока baseline-скану ZAP каже це прямо: «By default all alerts found by ZAP will be treated as WARNings.» Коди виходу там же: «0: Success ... 1: At least 1 FAIL ... 2: At least one WARN and no FAILs ... 3: Any other failure» — тобто сама лише знахідка теж дає ненульовий код, просто інший. І окремий прапорець, який це падіння знімає: «-I do not return failure on warning».
Місце друге: обгортка ігнорує код виходу контейнера. README обох GitHub-дій — і baseline, і повного скану — каже слово в слово: «By default ZAP Docker container will fail with an exit code, if it identifies any alerts. Set this option to true if you want to fail the status of the GitHub Scan if ZAP identifies any alerts during the scan.»
Це місце й ламає інтуїцію найчастіше. Контейнер справді віддає ненульовий код навіть на самих лише WARN — але крок GitHub-дії від цього не падає, поки не виставлено fail_action: true. Мовчазний зелений крок при цьому виглядає рівно як пройдений гейт. Дзеркальна деталь: з fail_action: true крок червоніє на будь-яких алертах, а не тільки на переведених у FAIL.
Місце третє: крок відпрацював, а інструмент — ні. Порожній звіт сканера коду може означати «не запустилося»; гейт цього стану не розрізняє.
Четверте місце — не про мовчання, а про хибне читання червоного. Аудит залежностей npm має параметр, що задає мінімальний рівень вразливості, з якого команда повертає ненульовий код (info / low / moderate / high / critical / none). Але ненульовий код буває й тоді, коли фікс існує: якщо політика свіжості релізу не дає поставити пропатчену версію, «npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code». Тобто червоний гейт може означати не «зʼявилася нова вразливість», а «фікс є, але його заблокувала ваша ж політика». Ланцюг постачання й пріоритизація знахідок — окрема глава розділу.
Корисний побічний сигнал того самого джерела: незвично великий обсяг 401/403 у прогонах варто виносити окремо — це натяк, що функціональна зміна зачепила наявні контролі доступу.
Чого канон не дає, знати корисно: порогу «який рівень знахідки валить білд» не встановлює жодне з цих джерел — вони описують механіку (конфіг правил, -I, коди виходу, fail_action, мінімальний рівень аудиту), а політику лишають команді. Немає ні процедури винятку повз гейт, ні межі «блокуючий гейт проти попереджувального». Загальне питання «що взагалі блокує мердж» — тема розділу про Git і CI/CD.
Типові помилки
- Виглядає як «сканер у пайплайні є, отже, безпека перевіряється». Насправді типово всі алерти ZAP — WARN, а крок дії не падає без
fail_action: true. - Виглядає як перевірка прав: негативний асерт на
res.ok(). Насправдіok()— це діапазон 200–299, тож403і404зливаються в одне «не вдалось». - Виглядає як перевірка прапорців кукі. Насправді читання через обʼєкт заголовків бачить одне значення на імʼя, а
Set-Cookieу відповіді буває кілька. - Виглядає як перевірка сесійної кукі одразу після логіну. Насправді кукі виставляються через ланцюг редиректів, і асерт до кінцевого URL перевіряє проміжний стан.
- Виглядає як контроль доступу: кнопки немає в інтерфейсі. Насправді це приховування в UI, яке обходиться тривіально.
- Виглядає як «зʼявилася нова вразливість»: червоний аудит залежностей. Насправді фікс може існувати, а заблокувала його політика свіжості релізу.
- Виглядає як «код чистий»: порожній звіт сканера. Насправді так само виглядає невідпрацьований інструмент — або сканер, який не достукався до застосунку в сусідньому контейнері.
Підсумок
- Security-асерт — це асерт на відповідь, а не на екран. Статус замість
ok(), масив заголовків замість обʼєкта, запит до сервера замість читання UI. - Матриця авторизації живе у файлі, а не в тестах. Тести беруть її входом; падіння називає комбінацію «сервіс + POV + фактичний код + очікуваний». Формат канон описує двома шпаргалками, які вимагають протилежного.
- Baseline безпечний, повний скан — . Перший придатний навіть проти продакшну; другий вимагає дозволу, попередження хостингу й розуміння, що форми будуть засабмічені.
- Гейт — це рішення про код виходу, і воно проходить три місця: як сканер класифікував знахідку (типово WARN, плюс прапорець
-I), чи валить обгортка статус кроку (fail_action), чи є перевірка required check. Мовчить будь-яке — знахідка не блокує мердж. - Порогу severity канон не дає. Механіка є, політика — рішення команди; вигадана цифра гірша за чесне «джерела цього не задають».
Можливі питання
- «Як ви перевіряєте заголовки безпеки й прапорці кукі в автотестах?» Слухають не назву методу, а те, чи знаєте ви про множинні
Set-Cookieй про момент перевірки після редиректів. - «Чим перевірка доступу в тесті відрізняється від перевірки, що кнопки немає?» Перевіряють розуміння, що клієнтські контролі обходяться тривіально. Сильна відповідь одразу згадує різницю
401і403. - «Як би ви автоматизували матрицю ролей?» Очікують не список тестів, а артефакт: файл із ролями й сервісами, генерація кейсів «сервіс × POV», обидві половини — дозволено й заборонено — і читабельне падіння.
- «Що поставите в CI: baseline чи повний скан?» Питання про ціну: baseline не атакує, повний скан — DAST з активними атаками, сабмітом форм і павуком без ліміту часу.
- «У нас зелений security-крок. Що це доводить?» Найкраще питання розділу. Правильна відповідь — «майже нічого, поки не перевірено три речі»: як класифіковано знахідки (WARN чи FAIL) і чи не знято падіння прапорцем, чи ввімкнено падіння статусу в обгортці, чи інструмент узагалі відпрацював і побачив ціль.
- «Який рівень вразливості має валити білд?» Пастка на вигадану цифру: джерела порогу не задають, вони дають механіку. Сильний кандидат каже це прямо й переводить розмову на те, як таку політику ухвалює команда.
Джерела
- OWASP DevSecOps Guideline — мета «detect as fast as possible» для дефекту дизайну та вразливості застосунку.
- OWASP DevSecOps Guideline — latest — модель «додані кроки в наявний пайплайн», порядок стадій від threat modeling до compliance і шість підрозділів стадії сканування.
- OWASP Cheat Sheet — CI/CD Security — вимога вбудувати SAST/DAST у пайплайн, ручний апрув перед продом і заборона довіряти дефолтам вендора.
Security-асерти: предмет перевірки — відповідь, а не екран
- Playwright — APIResponse class —
status,ok,headersArray,securityDetails: які поля відповіді доступні асерту і де межа кожного. - Playwright — API testing — санкція на перевірку постумов на сервері після дії в браузері.
- Playwright — Authentication — кукі виставляються через ланцюг редиректів; чекати кінцевого URL, а не фіксованої паузи.
- Playwright — Network — видалення заголовка перед
route.continue()як спосіб перевірити серверний рубіж. - Selenium — Working with cookies — вбудовані методи WebDriver для кукі й обмежений набір серіалізовних полів.
- OWASP Cheat Sheet — REST Security — коди відмови, дозволені методи, вимоги до JWT, порядок кроків процесу й заборона технічних деталей у помилці.
- OWASP Cheat Sheet — Authorization Regression Testing — приховування в UI не є контролем доступу: перевірку має робити API.
- OWASP Cheat Sheet — Authorization Testing Automation — навіщо матриця переживає реліз, два виміри й POV, три блоки файлу-півота, формат «spreadsheet» і вимоги до падіння.
- OWASP Cheat Sheet — Authorization Regression Testing — проблема «другого дня», вимога машиночитного формату, три патерни , контракт як джерело кейсів і структура набору.
- OWASP Cheat Sheet — REST Security — контроль доступу на кожному ендпоінті, бо кожен виклик REST окремий і stateless.
ZAP у пайплайні: baseline проти повного скану
- OWASP ZAP — Baseline Scan — що це за скрипт, павук плюс пасивний скан без атак, конфіг-файл
-gі перекваліфікація правил, progress-файл. - OWASP ZAP — Docker — образ
bareбез пакетних сканів, API Scan за OpenAPI/GraphQL, ізоляція контейнерів. - zaproxy/action-baseline — README — дія як обгортка: issue з алертами, звіт артефактом і штатний токен дії.
- zaproxy/action-full-scan — README — повний скан як DAST, попередження про атаку, узгодження з хостингом і CDN, сабміт форм.
- GitHub Docs — Code scanning — означення, дві задачі й тригери, життєвий цикл алерта, CodeQL і сторонні інструменти через SARIF, хвилини й ліцензія, сторінка стану інструментів.
Гейти: коли знахідка валить білд
- OWASP Cheat Sheet — Authorization Regression Testing — сенс набору лише разом із блокуванням, required check і сигнал за обсягом
401/403. - OWASP ZAP — Baseline Scan — дефолт WARN, конфіг правил, прапорець
-Iі чотири коди виходу. - zaproxy/action-baseline — README — формулювання про ненульовий код контейнера й вимогу
fail_action. - zaproxy/action-full-scan — README — те саме формулювання слово в слово для повного скану.
- npm CLI — config — мінімальний рівень для ненульового коду й ненульовий код при заблокованому фіксі.
- GitHub Docs — Code scanning — сторінка стану інструментів і витрата хвилин Actions.
Що таке security-асерт і чим він відрізняється від звичайного асерту?
Технічно — нічим, крім предмета. Це та сама перевірка в тому самому , просто дивиться вона не на текст на екрані, а на властивість відповіді, від якої залежить безпека: код , прапорці кукі, заголовок, стан ресурсу на сервері після дії. Окремого жанру «тест на безпеку» тут не заводять — заводять звичайний тест із іншим предметом . Практичний наслідок один, але важливий: автотест не вміє перевіряти абстрактну «захищеність», тож будь-яку вимогу спершу треба перекласти у конкретне поле відповіді. Поки вимога звучить як «чужі дані не мають бути доступні», асерту немає; коли вона звучить як «GET на чужий рахунок повертає 403» — асерт є.
Куди в пайплайні ставлять security-перевірки і чим визначається місце конкретної?
Канон OWASP не будує окремий «процес безпеки» збоку від наявного (pipeline) — він додає кроки в той самий пайплайн, а мету формулює через швидкість: дефект дизайну чи мають знайтися якомога раніше. Порядок стадій задає й порядок кроків: ще до коду, далі pre-commit із керуванням секретами й лінтингом, далі власне сканування (, , IAST, , інфраструктура, ), а наприкінці — аудит відповідності. Місце конкретної перевірки визначає не «рівень», а її вхід: аналізатору коду потрібен репозиторій, динамічному сканеру — жива ціль, до якої він достукається по мережі, асерту — вже отримана відповідь сервера. Про ці джерела не говорять узагалі, тож відповідь «security-тести живуть на рівні інтеграційних» — вигадка, а не канон. Ще дві речі, які варто згадати: перед у прод лишають ручний апрув, а дефолтним налаштуванням вендора сліпо не довіряють.
Чому в тесті на заборонений доступ не можна асертити res.ok()?
Бо ok() — це не «сервер повівся правильно», а буквально «код у діапазоні 200–299». Для перевірки прав це фатально: і 403, і 404, і 401, і навіть 500 дадуть однакове false, а різниця між ними — власне предмет тесту. Тест із таким асертом лишиться зеленим і тоді, коли сервер зламався й віддає 500 замість чесної відмови. Перевіряти треба status() і конкретне число з контракту: 403, якщо продукт визнає існування ресурсу, або 404, якщо він навмисно приховує сам факт його існування. Той самий асерт заодно ловить дзеркальну знахідку — коли замість відмови приходить 200.
Як правильно перевірити прапорці сесійної кукі у відповіді?
Тільки через масив заголовків, не через обʼєкт. Обʼєктна форма зводить заголовки до пар «імʼя — значення», тож із кількох Set-Cookie в одній відповіді тест побачить рівно один; решта зникає мовчки, без помилки й без попередження. А Set-Cookie — саме той заголовок, який дока наводить прикладом множинного: в масиві він зʼявляється стільки разів, скільки його надіслав сервер. Виходить найгірший різновид зеленого тесту: перевірка HttpOnly пройшла, але не на тій куці. Правильно взяти масив заголовків, відфільтрувати всі записи з іменем set-cookie (порівняння імені — без урахування регістру) і перевірити прапорці на кожному або хоча б адресно на потрібному. У WebDriver ситуація простіша: там для цього є вбудовані методи — прочитати одну куку за іменем або забрати всі одразу з поточної сесії, — а перелік полів, які повертаються, задає сама специфікація.
Тест логіниться і одразу перевіряє сесійну куку. Іноді червоний. Чому?
Майже завжди — через момент асерту. Логін рідко буває одним запитом: типово це ланцюг редиректів, і кукі виставляються в різних його ланках. Асерт, зроблений одразу після сабміту форми, перевіряє проміжний стан, у якому потрібної кукі ще немає, — звідси й плаваюче падіння, що залежить від швидкості середовища. Дока Playwright підписує цю пастку прямо в прикладі логіну, а лікування пропонує не паузою: треба дочекатися кінцевого URL флоу й перевіряти вже після нього. Фіксований waitForTimeout тут не рішення, а маскування — на повільному CI він знову не встигне. Побічний ефект того самого ланцюга є і в TLS-даних: для редиректу повертаються дані останнього запиту, а для не-HTTPS — null, тож перевірка сертифіката без перевірки самої наявності даних мовчки проходить на порожнечі.
Кандидат каже: «кнопки в інтерфейсі немає, отже, доступ закрито». Що з цим не так?
Це не контроль доступу, а приховування в UI, і воно обходиться без жодних інструментів — прямим запитом до з валідною сесією звичайного користувача. OWASP формулює це як вимогу: рубіж має тримати API, а стан інтерфейсу перевіркою прав не є. Для автотесту звідси випливає конкретна дисципліна: сценарій «елемента не видно» доповнюється сценарієм «прямий виклик відповідає 403 або 404». Є й акуратний прийом перевірити, хто саме тримає рубіж: у Playwright офіційний приклад зміни запиту — це видалення заголовка перед route.continue(). Прибрали службовий заголовок, який слухняно шле клієнт, і дивимось, чи сервер усе ще відмовляє; якщо доступ раптом зʼявився — контроль жив на клієнті.
Які негативні коди варто покривати в API-тестах, крім 401 і 403?
Шпаргалка OWASP REST Security дає готовий перелік, з якого очікування виводяться майже механічно. 405 — виклик методом поза списком дозволених для ресурсу. 429 — перевищення дозволеної частоти запитів. 413 — завеликий пейлоад. 406 або 415 — запит із неочікуваним чи взагалі відсутнім типом вмісту (виняток джерело робить тільки для порожнього тіла). Плюс базова пара: 401 означає, що автентифікаційних даних немає або вони криві, 403 — що користувача впізнали, але прав на цей ресурс у нього немає. І окремий пункт, який часто забувають: перевіряти треба не лише код, а й тіло. , назва фреймворку, шлях у файловій системі чи текст SQL-помилки — це знахідка навіть тоді, коли статус ідеально правильний.
Що перевіряють у JWT автотестами?
Найперше — що сервер не приймає незахищений токен з alg: none і не обирає алгоритм перевірки з заголовка самого токена: інакше підпис перевіряється тим, що запропонував нападник. Далі — обовʼязковий мінімум клеймів: iss, aud, exp, nbf; токен без них або з простроченим exp має бути відхилений. Найцікавіший сценарій — запит валідним за формою токеном уже після логауту: підписаний токен цілком здатен пережити власну сесію, бо ні натиснутий вихід, ні спрацьований самі по собі підпису не псують, — і саме тут видно, чи спирається сервер лише на криптографію, чи ще й на серверний стан сесії. Перевіряти це зручно на рівні API: логін, збереження токена, логаут, той самий токен у заголовку — і асерт на відмову, а не на 200.
Що таке перевірка порядку кроків процесу і чому звичайні тести прав її не замінюють?
Це перевірка того, чи можна викликати ендпоінти багатокрокового флоу поза послідовністю: одразу підтвердити замовлення, не оплативши; повернутися на попередній крок після завершення; перевикористати токен одного кроку на іншому. Тести ролей цього не ловлять структурно: кожен виклик REST авторизується самостійно й нічого не знає про те, які кроки вже пройдено, тож усі виклики можуть бути «дозволені» окремо, а разом складатися в обхід процесу. Практично це виглядає як два питання до кожного ендпоінта ланцюга: чи валідує він поточний стан і чи не приймає він артефакт, виданий для іншого кроку. Автоматизується це звичайним API-тестом, який просто робить кроки не в тому порядку й асертить відмову.
Навіщо матрицю авторизації виносити в окремий файл, а не писати тести руками?
Бо ламається авторизація не на старті, а в наступних релізах: фічі додають і змінюють, не оцінюючи, як це зачепило наявні права — саме це джерело називає головною причиною проблем із авторизацією, а надбудова над ним говорить про проблему «другого дня». Матриця у файлі перетворює правила з тексту в артефакт, який читають і людина, і програма: правил у собі тести не дублюють — вони читають файл і множать сервіси на . Наслідків два. Перший — нову роль чи новий сервіс додають правкою одного файлу, а не десятком нових тестів. Другий — перестає бути вибірковим: у файлі видно порожні клітинки, а в купі рукописних тестів не видно нічого. І дозволені клітинки набір перевіряє нарівні з забороненими — інакше зеленітиме на сервері, який відмовляє геть усім.
Що таке POV і як має виглядати падіння тесту, згенерованого з матриці?
POV (Point Of View) — це роль, від імені якої йде конкретний прогін: анонім, звичайний користувач, менеджер, адміністратор. Один тест-кейс на точку зору дає корисний побічний ефект — результати профілюються за рівнем доступу, і одразу видно, чи «протікає» саме анонім, чи вже автентифікований користувач. Падіння має бути самодостатнім: воно називає сервіс, точку зору, фактичний код і очікуваний — на кшталт «сервіс видалення повідомлення, POV анонім: отримали 200, очікували 403». Це не косметика: у згенерованому наборі сотні кейсів, і повідомлення «expected 403, received 200» без координат змушує лізти в код генератора замість того, щоб іти до розробника з готовим фактом. У Playwright це робиться другим аргументом до expect — повідомленням, зібраним із тих самих полів матриці.
У якому форматі зберігати матрицю — таблиця чи JSON/YAML?
Тут канон суперечить сам собі, і найсильніша відповідь — знати про це, а не обирати сторону навмання. Шпаргалка Authorization Testing Automation описує матрицю саме як таблицю (spreadsheet), а приклад тримає у XML. Шпаргалка Authorization Regression Testing вимагає прямо протилежного: зберігати матрицю в машиночитному форматі — JSON, YAML або структуровані — саме щоб фреймворк генерував кейси динамічно й ручна підтримка не росла разом із політикою. Обидва документи чинні, причому другий позиціює себе як надбудову над першим і додає до нього вимір безперервної . Практична користь від цього знання дуже приземлена: суперечку на ревʼю «чому в нас YAML, а не табличка» вирішує цитата з потрібного документа, а не аргументи про зручність.
Які патерни регресії авторизації ганяють по матриці?
Три. (вона ж ): користувач B тієї самої ролі намагається прочитати, змінити й видалити ресурс користувача A — очікується 403 або 404, і ніколи 200. : усі неадміністративні точки зору, включно з анонімною, стукають в адміністративні ендпоінти; перевірку тут робить API, а сховані в інтерфейсі кнопки не рахуються взагалі. : користувач одного тенанта робить широкий читальний запит по даних іншого, і критерій провалу тут найжорсткіший — досить одного чужого ідентифікатора запису у відповіді, щоб вважати це критичним збоєм. Окремий важіль поверх цих трьох — контракт: якщо специфікація вимагає для ендпоінта конкретний , набір має сам перевіряти, що токен без нього отримує 401 або 403. І перевіряти це треба на кожному ендпоінті, бо кожен виклик REST авторизується окремо.
Що поставити в CI — ZAP baseline чи повний скан?
Це питання про ціну, а не про «який кращий». Baseline пускає павука по цілі (типово хвилину), чекає завершення і жодних не виконує — тому він триває кілька хвилин, ставиться в кожен прогін і придатний навіть проти продакшну. Повний скан — це вже DAST з активним скануванням без ліміту часу для павука, тобто справжні атаки по цілі; його README відкривається окремим попередженням саме про це. Побічні ефекти теж різні: активний скан сабмітить форми, тож форма зворотного звʼязку й коментарі полетять реальними повідомленнями живим людям. Звідси й практична різниця: baseline безпечний для регулярного прогону, а повний скан у кожному — це довго й шумно, тож він потребує дозволу на ціль і попередження не лише власника застосунку, а й хостингу з CDN. Конкретного розкладу для повного скану (нічний прогін, перед релізом, щотижня) джерела не задають — це вже домовленість команди. Поза цією парою існує ще й окремий скан під API: активний прогін по ендпоінтах, які описує OpenAPI або GraphQL.
ZAP у пайплайні нічого не знайшов. Які є пояснення, крім «застосунок чистий»?
Найчастіше — сканер просто не достукався до цілі. За замовчуванням застосунок в одному контейнері Docker не бачить застосунку в іншому, тож у типовій CI-збірці «сканер у контейнері, застосунок у сусідньому» прогін проходить успішно й порожньо. Друге пояснення — : найлегший bare пакетних сканів взагалі не містить, тож запуск може не робити того, що ви думаєте. Третє — конфігурація правил: після першого прогону генерують конфіг-файл параметром -g, і в ньому кожне правило можна перевести у IGNORE; надто щедрий IGNORE робить звіт порожнім законно. До речі, відомий борг правильніше виносити не в IGNORE, а в progress-файл, де знахідки позначаються як IN-PROGRESS: тоді у звіті видно саме нові, а не тишу. І окремо варто памʼятати, що GitHub-дії — не інший сканер: вони обгортають ті самі пакетні скани, ведуть issue з алертами й кладуть звіт артефактом.
Що таке code scanning на GitHub і до чого тут SARIF?
Code scanning — функція GitHub для аналізу коду прямо в репозиторії: вона шукає вразливості безпеки й помилки кодування, а знахідки показує там, де лежить сам код. Працює вона на два фронти — розгрібає борг, який уже накопичився, і не пускає новий, — а тригером служить пуш, інша подія репозиторію або розклад. Результат вимірюється алертами, і закриваються вони самі: щойно виправлено код-причину, алерт зникає. Ключове для розуміння меж: це радше майданчик для аналізаторів, ніж конкретний аналізатор — поруч із CodeQL підключається будь-який сторонній інструмент, навіть запущений у чужому CI, бо сумісність забезпечує спільний формат обміну результатами (Static Analysis Results Interchange Format, ). Обмеження теж варто назвати: прогін їсть хвилини GitHub Actions, приватним репозиторіям потрібна ліцензія Code Security, а порожній звіт двозначний — так само виглядає і чистий код, і аналізатор, який не відпрацював; ці випадки розводить окрема сторінка стану інструментів.
Security-крок у пайплайні зелений. Що це доводить?
Саме по собі — майже нічого, поки не перевірені чотири речі. Перша: як інструмент класифікував знахідки. У ZAP усі алерти типово вважаються попередженнями, а не помилками, і в FAIL їх переводить рішення по кожному правилу в конфізі; плюс існує -I, який знімає падіння на попередженнях узагалі. Друга: чи читає обгортка код виходу контейнера. Обидві GitHub-дії ZAP за замовчуванням не валять статус кроку, навіть коли контейнер повернув ненульовий код, — для цього треба виставити fail_action: true (і памʼятати дзеркальний ефект: тоді крок червоніє на будь-яких алертах, а не лише на переведених у FAIL). Третя: чи є перевірка required check — бо червоний крок, який не блокує , гейтом не є; сенс набору регресії авторизації існує рівно доти, доки він не пускає вразливий код у мердж. Четверта: чи інструмент узагалі відпрацював і побачив ціль — крок, який зеленіє на невдалому запуску, виглядає точно як пройдений гейт. Мовчить будь-яка з цих ланок — знахідка не блокує нічого.
Червоний npm audit у пайплайні — це завжди нова вразливість?
Ні, і це друга половина тієї самої пастки з кодами виходу. Аудит має параметр, який задає мінімальний рівень вразливості для ненульового коду виходу — від info до critical, з окремим значенням none, — тож червоний колір спершу треба звірити з тим, що саме команда домовилася вважати падінням. Але є й окремий випадок: ненульовий код приходить і тоді, коли фікс існує, просто політика свіжості релізу не дозволила поставити пропатчену версію — тоді пакет лишається вразливим, а команда попереджає, що виправлення заблоковано. Тобто червоне може означати не «зʼявилася нова діра», а «фікс є, але його не пустила ваша ж конфігурація». Практичний висновок: перш ніж заводити тікет на залежність, читайте текст виводу, а не колір кроку.
Який рівень вразливості має валити білд?
Це питання-пастка на вигадану цифру. Джерела, які описують цю механіку, порогу не задають: вони дають інструменти — класифікацію правил у конфізі, прапорець зняття падіння, коди виходу, перемикач падіння статусу в обгортці, мінімальний рівень для аудиту залежностей, — а політику лишають команді. Так само в них немає ні описаної процедури винятку повз гейт, ні межі між блокуючим і попереджувальним гейтом. Сильна відповідь на співбесіді каже це прямо, а далі переводить розмову в площину процесу: хто ухвалює поріг, як оформлюється тимчасовий виняток, куди девається відомий борг (у progress-файл, а не в глухий IGNORE) і як команда відрізняє «нова знахідка» від «стара, вже прийнята». Назвати навмання «high і вище валить білд» гірше, ніж чесно сказати, що канон цього не встановлює.
Три кейси про те, як інваріант безпеки стає кодом і чому зелений колір тут коштує дешево: розтин трьох , які виглядали як перевірка безпеки й нею не були; матриця ролей, винесена у файл, з якого генеруються кейси; і покроковий аудит security-кроку в CI, що не блокує нічого. Скрізь — що дивитися і чому.
Кейс 1. Тест, який брехав: три асерти на відповідь
У є тест «менеджер не має доступу до чужого рахунку». Він зелений півроку. Насправді він зелений незалежно від того, працює контроль доступу чи ні. Ось як він виглядав:
// БУЛО: три асерти, жоден з яких не перевіряє того, що написано в назві тесту
test('менеджер не бачить чужий рахунок', async ({ page, request }) => {
await page.goto('/accounts/9931');
await expect(page.getByTestId('withdraw-button')).toBeHidden(); // 1
const res = await request.get('/api/accounts/9931', {
headers: { Authorization: `Bearer ${managerToken}` },
});
expect(res.ok()).toBeFalsy(); // 2
const cookie = res.headers()['set-cookie'];
expect(cookie).toContain('HttpOnly'); // 3
});
Той самий тест після ремонту:
// СТАЛО: предмет кожного асерту — конкретне поле відповіді
test('менеджер не бачить чужий рахунок', async ({ page, request }) => {
const res = await request.get('/api/accounts/9931', {
headers: { Authorization: `Bearer ${managerToken}` },
});
// 403 — впізнали, але не можна; 404 був би теж валідним, якщо так каже контракт
expect(res.status(), 'чужий рахунок віддали замість відмови').toBe(403);
// у тілі відмови не має бути технічних деталей
const body = await res.text();
expect(body).not.toMatch(/stack|Traceback|at .+\(.+:\d+:\d+\)/i);
// прапорці — з масиву: Set-Cookie у відповіді буває кілька
const setCookie = res
.headersArray()
.filter((h) => h.name.toLowerCase() === 'set-cookie');
for (const h of setCookie) {
expect(h.value, `кука без HttpOnly: ${h.value.split('=')[0]}`).toMatch(/HttpOnly/i);
}
});
Що дивитися і чому:
- Асерт 1 перевіряв дизайн, а не право. Схована кнопка означає рівно те, що її не намалювали; при цьому доступний прямим запитом із валідною сесією. Рубіж має тримати API, тож сценарій «елемента не видно» — доповнення до перевірки доступу, а не її заміна.
- Асерт 2 приймав будь-яку катастрофу за успіх. успішності — це діапазон 200–299, тож
403(правильна ),404,401і500(сервер упав) дають однаковеfalse. Тест лишався б зеленим і після того, як ендпоінт почав падати з винятком. Число з контракту вstatus()розводить ці випадки. - Асерт 3 перевіряв не ту куку. Обʼєктна форма заголовків зводить їх до пар «імʼя — значення», а
Set-Cookieв одній відповіді буває кілька — саме цей заголовок дока наводить прикладом множинного. Перевірка мовчки бачила одну з них; масив заголовків повертає всі записи, і саме на масиві прапорці перевіряються чесно. - Момент перевірки — окрема пастка. Якщо куку дивитися одразу після сабміту форми логіну, асерт може прийти в середину ланцюга редиректів, де потрібної кукі ще немає: тест буде плавати залежно від швидкості середовища. Чекати треба кінцевого URL флоу, а не фіксованої паузи.
- Хто саме тримає рубіж — перевіряється заголовка. Якщо клієнт слухняно шле службовий заголовок, приберіть його на льоту й подивіться, чи сервер усе ще відмовляє:
await page.route('**/api/**', async (route) => {
const headers = route.request().headers();
delete headers['x-admin-mode']; // клієнт більше не «підтверджує» свої права
await route.continue({ headers });
});
Кейс 2. Матриця ролей як файл, з якого ростуть тести
Класична задача: десяток ендпоінтів, чотири ролі, кожен реліз щось додає. Рукописні тести на це не масштабуються — і головне, у них не видно порожніх клітинок. Правила виносимо у файл, який читають і людина, і генератор (нижче — витяг на три сервіси).
{
"roles": [
{ "name": "ANONYMOUS", "headers": {} },
{ "name": "USER", "headers": { "Authorization": "Bearer {{token.user}}" } },
{ "name": "MANAGER", "headers": { "Authorization": "Bearer {{token.manager}}" } },
{ "name": "ADMIN", "headers": { "Authorization": "Bearer {{token.admin}}" } }
],
"services": [
{ "name": "ListInvoices", "method": "GET", "url": "/api/invoices",
"allowedRoles": ["USER", "MANAGER", "ADMIN"] },
{ "name": "DeleteInvoice", "method": "DELETE", "url": "/api/invoices/{{id.own}}",
"allowedRoles": ["ADMIN"] },
{ "name": "ExportTenant", "method": "GET", "url": "/api/tenants/{{id.tenant}}/export",
"allowedRoles": ["ADMIN"] }
],
"services-testing": [
{ "name": "DeleteInvoice", "payload": { "reason": "audit" } }
]
}
Генератор не знає доменної логіки — він знає лише, дозволена клітинка чи ні:
import matrix from './authorization-matrix.json';
import { resolve } from './placeholders'; // підставляє токени й ідентифікатори прогону
for (const service of matrix.services) {
for (const pov of matrix.roles) {
const allowed = service.allowedRoles.includes(pov.name);
const expected = allowed ? 200 : 403;
test(`${service.name} · POV ${pov.name}`, async ({ request }) => {
const res = await request.fetch(resolve(service.url), {
method: service.method,
headers: resolve(pov.headers), // підміна токена, не повторний логін
data: matrix['services-testing'].find((s) => s.name === service.name)?.payload,
});
expect(
res.status(),
`${service.name} · POV ${pov.name}: очікували ${expected}`,
).toBe(expected);
});
}
}
Падіння читається без відкривання коду:
1) DeleteInvoice · POV ANONYMOUS
Error: DeleteInvoice · POV ANONYMOUS: очікували 403
Expected: 403
Received: 200
, за якою заповнюється колонка «очікуємо»:
| клітинка | очікуваний результат | чому саме так |
|---|---|---|
| роль у списку дозволених | успіх за контрактом ендпоінта | дозволена половина перевіряється нарівні із забороненою: інакше тест зеленіє на зламаному сервері, який відмовляє всім |
| роль поза списком | 403 або 404 | який саме з двох — питання до контракту продукту: 404 приховує сам факт існування ресурсу |
| анонім на будь-який непублічний сервіс | 401 або 403 | немає автентифікації — 401; впізнали, але не можна — 403 |
| чужий обʼєкт тієї самої ролі (IDOR) | 403 або 404, ніколи 200 | горизонтальна ескалація: ідентифікатор у URL підмінюється тривіально |
| широкий запит по даних чужого тенанта | порожньо або відмова | навіть один чужий ідентифікатор запису у відповіді — критичний збій, а не «дрібне протікання» |
| клітинка не заповнена | заборонено | незаповнена комбінація читається як відмова, а не як «ще не вирішили» |
Що дивитися і чому:
- Файл — джерело істини, тести — похідне. Нова роль додається одним записом у
roles, і набір одразу перевіряє її на всіх сервісах. Правило, переписане в код тесту, живе окремо від правила в голові команди — і розходиться з ним уже на другому релізі. - Формат обирає не смак, а джерело. Одна шпаргалка OWASP описує матрицю як таблицю з прикладом у XML, друга прямо вимагає машиночитного формату — JSON, YAML або структурованих — саме заради динамічної генерації. Обидві чинні, тож на ревʼю це закривається посиланням, а не суперечкою.
- Перемикання ролі — підміна токена. Джерело вимагає саме дешевого перемикання контексту автентифікації, без повного логіну на кожен тест: повний логін на кожну комбінацію роздуває прогін і додає власних причин для .
- Набір йде в тому самому , що функціональний, і виділяється тегом чи окремим скриптом — щоб локально ганяти вибірково, а в CI цілком.
- Сигнал на межі теми: різкий стрибок кількості
401/403у прогоні заслуговує окремого рядка у звіті. Так виглядає функціональна правка, яка ненароком переставила комусь права, — навіть коли жоден тест не почервонів.
Кейс 3. Розтин зеленого security-кроку
Пайплайн має крок зі сканером, він зелений уже кілька місяців, і команда вважає, що безпека перевіряється. Аудит починається не зі сканера, а з питання «хто тут узагалі читає результат як падіння».
- name: ZAP baseline
uses: zaproxy/action-baseline # версію дії закріпіть за вашою політикою
with:
target: https://staging.example.com
rules_file_name: .zap/rules.tsv
cmd_options: '-I' # ← знімає падіння на попередженнях
# fail_action не виставлено ← крок не почервоніє навіть на ненульовому коді
Файл правил після генерації параметром -g виглядає так (ідентифікатори підставляє сам ):
# ідентифікатор рішення назва правила
NNNNN IGNORE (Timestamp Disclosure)
NNNNN WARN (X-Content-Type-Options Header Missing)
NNNNN FAIL (Content Security Policy (CSP) Header Not Set)
Коди виходу самого скрипта:
| код | що це означає |
|---|---|
| 0 | успіх — жодна знахідка не прочитана як падіння |
| 1 | є щонайменше один FAIL |
| 2 | є попередження, FAIL немає |
| 3 | інший збій запуску |
Далі — чотири питання, і зелений колір має пережити всі чотири:
| ланка | питання | у прикладі вище |
|---|---|---|
| класифікація знахідки | правило у WARN, FAIL чи IGNORE? | типово все — WARN, у FAIL переведено одне правило |
| код виходу | чи не знято падіння на попередженнях? | знято прапорцем -I |
| обгортка | чи валить крок статус на алертах? | ні, fail_action не виставлено |
| статус перевірки | чи є крок required check? | якщо ні — червоне не блокує мердж |
Що дивитися і чому:
- Дефолт працює проти вас. Усі знахідки ZAP типово вважаються попередженнями, а не помилками, і рішення «що з цим робити» ухвалюється по кожному правилу окремо — у згенерованому файлі. Поки цього файлу немає, ви не налаштували гейт, ви прийняли чужий дефолт.
- Прапорець
-Iі відсутнійfail_action— дві незалежні заглушки. Перша не дає скрипту повернути падіння на попередженнях, друга не дає обгортці прочитати ненульовий код . Прибрати треба обидві, і памʼятати дзеркальний ефект: увімкненийfail_actionвалить крок на будь-якому алерті, включно з тими, що лишилися попередженнями. - Червоний крок — ще не гейт. Поки перевірка не є обовʼязковою (required check), падіння нічому не заважає: мерджиться поверх червоного. Сенс набору авторизації існує рівно доти, доки він не пускає вразливий код у .
- Найтихіша ланка — інструмент, який не відпрацював. Контейнер за замовчуванням не бачить застосунку в сусідньому контейнері, а найлегший
bareпакетних сканів узагалі не містить: прогін «успішний і порожній» на вигляд не відрізняється від чистого застосунку. Те саме зі сканером коду — порожній звіт має два пояснення, і гейт їх не розводить. - Відомий борг — у progress-файл, не в IGNORE. Позначка
IN-PROGRESSлишає знахідку у звіті, але прибирає її з нових; глухий IGNORE прибирає її з поля зору назавжди, разом із регресією, яка колись прийде тим самим правилом. - Червоний аудит залежностей читайте текстом, а не кольором. Ненульовий код виходу приходить і тоді, коли фікс існує, але його заблокувала політика свіжості релізу — тікет «нова вразливість» у такому разі буде неправдою. А порогу «яка severity валить білд» жодне з цих джерел не задає: інструменти вам дали, домовлятися про межу доведеться самим.
Місце в пайплайні
- Розумію модель «додані кроки», а не «окремий процес поруч»: security-перевірки вбудовують у наявний заради швидкості знахідки, а перед у прод лишають ручний апрув, і дефолтні налаштування вендора апріорі довірою не користуються.
- Можу назвати порядок стадій ( до коду → pre-commit із секретами й лінтом → сканування: , , IAST, , інфраструктура, → аудит відповідності) і памʼятаю, що рівня в канон цим перевіркам не призначає: місце визначає вхід перевірки — репозиторій, піднятий застосунок чи відповідь сервера.
Асерт на відповідь, а не на екран
- Розумію, що предмет — поля відповіді (код, заголовки, кукі, TLS-дані, стан на сервері), а не стан інтерфейсу, і що виклик до HTTP-API з UI-тесту потрібен саме для перевірки постумов після дії в браузері.
- Знаю, чому
ok()не годиться для негативного сценарію: це лише «код між 200 і 299», тому правильна й падіння сервера повертають однаковеfalse— требаstatus()і конкретне число з контракту. - Памʼятаю, що прапорці кукі читаються тільки з масиву заголовків: обʼєктна форма лишає по одному запису на кожне імʼя заголовка, тоді як
Set-Cookieв одній відповіді буває кілька (саме його дока наводить прикладом множинного заголовка) — перевірка мовчки пройде не на тій куці. - Знаю дві часові пастки: сесійна кука виставляється через ланцюг редиректів (чекати кінцевого URL, а не фіксованої паузи), а TLS-дані для не-HTTPS повертаються порожніми й для редиректу описують останній запит ланцюга — «сертифікат ок» і «дані не дивилися» на вигляд однакові.
Негативні сценарії API
- Можу перелічити коди відмови як окремі очікування:
401— автентифікації немає або вона крива,403— впізнали, але прав немає,405— метод поза дозволеними,429— перевищення частоти,413— завеликий пейлоад,406/415— неочікуваний тип вмісту; і перевіряю не лише код, а й тіло, бо чи внутрішні деталі — знахідка навіть при правильному статусі. - Знаю мінімум перевірок JWT: відмова на незахищений токен (
alg: none), заборона брати алгоритм перевірки із заголовка самого токена, обовʼязкові клеймиiss,aud,exp,nbfі сценарій «запит валідним токеном після логауту». - Розумію, чому обхід порядку кроків процесу не ловиться тестами ролей: кожен виклик REST авторизується самостійно й нічого не знає про пройдені кроки, тож перевіряти треба валідацію поточного стану й токенів між кроками.
Матриця авторизації як код
- Можу пояснити причину виносити матрицю у файл — права ламає не перший реліз, а наступні: фічу додали чи змінили, впливу на дозволи не оцінили (проблема «другого дня»), тож прогін потрібен на кожен реліз, — і знаю анатомію: два виміри (фіча й логічна роль) плюс необовʼязковий третій (дані), роль під час прогону зветься (Point Of View, POV), а файл-півот складається з блоків
roles,servicesіservices-testing, де значення у фігурних дужках{}— плейсхолдери під час прогону. - Памʼятаю вимоги до набору: один кейс на точку зору (щоб результати профілювались за рівнем доступу), дозволений і заборонений доступ рівноправні, перемикання ролі — підміною токена, а не повторним логіном, і падіння називає сервіс, POV, фактичний та очікуваний код.
- Знаю, що канон про формат матриці суперечить сам собі: одна шпаргалка OWASP описує її як таблицю з прикладом у XML, друга вимагає машиночитного формату (JSON, YAML, структуровані ) саме заради динамічної генерації кейсів — і що друга позиціює себе надбудовою над першою.
- Можу назвати три патерни й критерій кожного: () — чужий ресурс дає
403чи404, але ніколи200; вертикальна — усі неадмінські POV, включно з анонімом, стукають в адмінські , і вердикт виносить API; — навіть один чужий ідентифікатор запису у відповіді вважається критичним збоєм.
ZAP у пайплайні
- Знаю, що baseline — це скрипт із Docker- (павук типово хвилину плюс ), він не виконує , триває лічені хвилини й придатний навіть проти продакшну.
- Розумію ціну повного скану: це DAST з активним скануванням і павуком без ліміту часу, тобто реальні атаки по цілі та сабміт форм — попереджати треба не тільки власника застосунку, а й хостинг із CDN; окремо існує активний скан по API, описаних OpenAPI або GraphQL.
- Памʼятаю дві деталі оточення, які роблять прогін безглуздим: образ
bareпакетних сканів не містить, а контейнер за замовчуванням не дістає застосунку в сусідньому контейнері — «нічого не знайшов» часто означає «не достукався».
Сканування коду й гейт
- Знаю, що code scanning — це місце для аналізаторів, а не один аналізатор: CodeQL або сторонній інструмент (у тому числі із зовнішнього CI) підключається через формат обміну результатами (Static Analysis Results Interchange Format, ), одиниця результату — алерт, який закривається сам після виправлення коду, а межі — хвилини Actions і ліцензія для приватних репозиторіїв.
- Можу назвати всі ланки, через які знахідка стає червоним : класифікація в конфізі (типово WARN, у FAIL чи IGNORE переводить рішення по кожному правилу з файлу, згенерованого параметром
-g;-Iзнімає падіння на попередженнях), перемикачfail_actionв обгортці GitHub-дії, статус перевірки як required check — і четверта, мовчазна: інструмент, який не відпрацював. - Памʼятаю, що порожній звіт неоднозначний за побудовою (чистий код і невідпрацьований інструмент виглядають однаково), а відомий борг виносять у progress-файл із позначкою
IN-PROGRESS, а не в IGNORE, щоб у звіті було видно саме нові знахідки. - Не читаю червоний аудит залежностей як «зʼявилася нова вразливість»: мінімальний рівень для ненульового коду виходу задається конфігом, а сам ненульовий код приходить навіть тоді, коли виправлення вже випущене, просто політика свіжості не пустила пропатчену версію — і памʼятаю, що порогу severity канон не встановлює взагалі, він дає лише механіку.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Що таке security-асерт (security assertion)?