---
tags: [практика, слушателю, задания, БРПО]
занятие: практикум, пары 2–5
---

# Практикум: от требований к проверяемому релизу

Четыре задания на четыре практические пары. Все они про **одно и то же приложение** — журнал учёта машинных носителей информации, который к вечеру пройдёт путь от тридцати строк до версии с тестами и проверками.

**Ничего не нужно писать с нуля.** В каждом задании готовый файл, который надо запустить, прочитать и изменить в нескольких местах.

## Прежде чем начать: какой у вас вариант

| | Вариант А — локально | Вариант Б — в браузере |
|---|---|---|
| Условие | На компьютере есть Python | Python поставить нельзя |
| Где работаем | VS Code или Блокнот + командная строка | onlinegdb.com или programiz.com |
| Что доступно | Все задания полностью | Задания 1–3, задание 4 частично |

Проверить вариант — одна команда в PowerShell:

```powershell
py --version
```

Если ответ вида `Python 3.11.x` или новее — у вас вариант А. Если ошибка — вариант Б, преподаватель покажет, куда зайти.

> [!note] Про кириллицу в выводе
> Если при запуске появится `UnicodeEncodeError` — это не ошибка вашего кода. Так проявляется кодировка консоли Windows. Во всех файлах курса от неё уже стоит защита: строка `sys.stdout.reconfigure(encoding="utf-8")`. Если пишете свой файл — добавьте её же.

---

## Задание 1. Рабочее место и первая версия

**Пара 2, 11:10–12:40. На выполнение — 35 минут.**
**Цель:** собрать рабочее место, взять код под контроль версий, запустить первую версию журнала.

### Шаг 1.1. Проверить инструменты (вариант А)

```powershell
py --version
git --version
```

Если git не найден — не страшно, шаги 1.3 и 1.6 посмотрим на экране преподавателя, остальное выполняется.

### Шаг 1.2. Перейти в каталог с кодом

Скачайте [архив материалов](https://27-07-2026.pikov.expert/materials.zip), распакуйте его в любую свою папку — например, в «Документы» — и перейдите в каталог `code` внутри распакованной папки.

```powershell
cd "$HOME\Documents\rbpo-practice\code"
```

Путь подставьте свой — тот, куда распаковали. Проверить, что вы на месте:

```powershell
dir step1_list.py
```

Файл должен найтись. Если нет — вы в другом каталоге.

### Шаг 1.3. Взять код под контроль версий

Сначала представьтесь git — иначе первый же коммит остановится с сообщением `Author identity unknown`. Это не ошибка, просто git не знает, кто вы:

```powershell
git init
git config --local user.name "Ваше Имя"
git config --local user.email "student@example.invalid"
```

Ключ `--local` означает «только для этого проекта»: настройки других репозиториев на компьютере не изменятся. Адрес можно указать любой, он никуда не отправляется.

Теперь первый коммит:

```powershell
git add .gitignore
git commit -m "Первый коммит: правила игнорирования"
```

**Вопрос, на который надо ответить до следующего шага:** почему первым коммитом идёт `.gitignore`, а не программа?

<details>
<summary>Проверьте себя после того, как ответите</summary>

Потому что всё, что один раз попало в git, остаётся в его истории навсегда. Удаление файла следующим коммитом не убирает его из предыдущих: `git log -p` покажет содержимое. Значит, правила «что не коммитить» должны действовать **до** первого коммита с кодом, а не после.

</details>

### Шаг 1.4. Запустить первую версию

```powershell
py step1_list.py
```

**Ожидаемый результат:**

```
Инв. №     Тип            Гриф             Ответственный
----------------------------------------------------------------
МНИ-001    USB-флеш       ДСП              Иванова А. П.
МНИ-002    HDD            Открыто          Петров С. И.
МНИ-003    USB-флеш       ДСП              Иванова А. П.

Всего записей: 3

Поиск «Иванова»:
  МНИ-001 — USB-флеш
  МНИ-003 — USB-флеш
```

### Шаг 1.5. Прочитать код и изменить его

Откройте `step1_list.py` и найдите в самом низу файла блок `if __name__ == "__main__":`. Там три строки `add(...)`, а под ними — `show()`.

**Важно: новые строки добавляйте сразу после трёх существующих `add(...)`, ДО строки `show()`.** Если дописать их в самый конец файла, они выполнятся уже после вывода таблицы, и в таблице ничего не изменится.

1. Добавьте четвёртый носитель — вставьте эту строку четвёртой, перед `show()`:

```python
    add("МНИ-004", "SSD", "Конфиденциально", "Сидорова М. К.")
```

Запустите. В таблице должно стать 4 записи, внизу — «Всего записей: 4».

2. **Теперь сломайте намеренно.** Добавьте пятую строку — тоже перед `show()`:

```python
    add("что угодно", "Дискета", "Совершенно секретно", "asdf")
```

Запустите ещё раз.

**Вопрос:** программа выдала ошибку? Появилась ли эта мусорная запись в таблице? Что это говорит о качестве контроля данных?

<details>
<summary>Проверьте себя</summary>

Никакой ошибки не будет: запись спокойно попадёт в журнал и отобразится в таблице. Программа работает — и при этом уже содержит дефект. Просто «работает» проверялось только на правильных данных. Это и есть переход к заданию 2.

</details>

### Шаг 1.6. Зафиксировать результат

```powershell
git add step1_list.py
git commit -m "Журнал учёта МНИ: первая версия"
git log --oneline
```

### Результат задания 1

- [ ] `step1_list.py` запускается и печатает таблицу
- [ ] Добавлена четвёртая запись
- [ ] Проверено, что мусорные данные проходят без сопротивления
- [ ] В репозитории минимум два коммита
- [ ] Есть ответ на вопрос про `.gitignore`

---

## Задание 2. Класс вместо словаря

**Пара 3, 13:30–15:00. На выполнение — 25 минут.**
**Цель:** увидеть, как проверка, встроенная в структуру данных, отличается от проверки при вводе.

### Шаг 2.1. Запустить вторую версию

```powershell
py step2_class.py
```

**Ожидаемый результат** — та же таблица, а затем блок:

```
Проверка контроля ввода:
  отклонено: Инвентарный номер 'XYZ-999' не соответствует шаблону МНИ-NNN
  отклонено: Тип 'Дискета' не входит в справочник: USB-флеш, HDD, SSD, CD/DVD, Карта памяти
  отклонено: Гриф 'Совершенно секретно' не входит в справочник: Открыто, ДСП, Конфиденциально
```

Сравните с тем, что произошло в шаге 1.5. Там те же данные прошли молча.

### Шаг 2.2. Разобрать три отличия

Откройте `step2_class.py` и найдите в коде:

1. Строку, из-за которой запись **нельзя изменить** после создания.
2. Метод, в котором происходит проверка **при создании** объекта.
3. Место, где журнал не даёт добавить **дубликат** инвентарного номера.

Выпишите номера строк — они понадобятся при разборе.

### Шаг 2.3. Добавить своё правило

Сейчас тип «Магнитная лента» журнал не примет — его нет в справочнике. Проверим это, а потом разрешим.

**Шаг а.** В блоке `if __name__ == "__main__":` добавьте строку сразу после трёх существующих `journal.add(...)`, перед `show(journal)`:

```python
    journal.add(Medium("МНИ-004", "Магнитная лента", "ДСП", "Сидорова М. К."))
```

Запустите. Программа остановится с `ValidationError`: тип вне справочника. Так и должно быть.

**Шаг б.** Теперь найдите вверху файла список `ALLOWED_KINDS` и допишите в него новый тип:

```python
ALLOWED_KINDS = ("USB-флеш", "HDD", "SSD", "CD/DVD", "Карта памяти", "Магнитная лента")
```

Запустите снова — запись создалась, в таблице четыре строки.

**Шаг в.** Добавьте **своё** правило: инвентарный номер `МНИ-000` считается зарезервированным и не принимается. Правило пишется в метод `__post_init__` класса `Medium`, рядом с остальными проверками.

Проверить своё правило можно так — добавьте временно перед `show(journal)`:

```python
    journal.add(Medium("МНИ-000", "SSD", "ДСП", "Сидорова М. К."))
```

Должна возникнуть ваша `ValidationError`. После проверки эту строку уберите.

<details>
<summary>Подсказка, если не получается</summary>

Проверка выглядит примерно так — вставьте её в `__post_init__` рядом с остальными:

```python
if self.inv == "МНИ-000":
    raise ValidationError("Инвентарный номер МНИ-000 зарезервирован")
```

</details>

### Шаг 2.4. Таблица по четырём языкам

Откройте по очереди четыре файла в каталоге `code/languages/` и заполните таблицу. Запускать нужно только питоновский: `py languages/copy_name.py`.

| Язык | Кто отвечает за границы памяти | Можно ли отключить проверку |
|---|---|---|
| Ассемблер | | |
| Object Pascal | | |
| C# | | |
| Python | | |

**Вопрос на осмысление:** назовите три дефекта, от которых не спасает **никакой** выбор языка.

### Результат задания 2

- [ ] `step2_class.py` запускается, три невалидные записи отклонены
- [ ] Найдены три места в коде из шага 2.2
- [ ] Добавлен новый тип носителя и своё правило проверки
- [ ] Заполнена таблица по четырём языкам
- [ ] Сделан коммит

---

## Задание 3. Найти и исправить четыре дефекта

**Пара 4, 15:10–16:40. На выполнение — 60 минут. Это главное задание дня.**
**Цель:** увидеть работающую уязвимость, назвать её класс и убедиться, что исправление действительно работает.

> [!important] Три файла — не перепутайте
> | Файл | Что с ним делать |
> |---|---|
> | `step3_defect.py` | **Образец. Не трогать.** Он нужен, чтобы всегда можно было посмотреть, как дефект выглядел изначально |
> | `step3_student.py` | **Ваш рабочий файл.** Его вы и правите. Это точная копия образца |
> | `step3_fixed.py` | Эталон решения. Открыть **только после** того, как сделаете сами |
>
> Если запутались и сломали свой файл — верните его в исходное состояние одной командой: `copy step3_defect.py step3_student.py`
>
> Проверять себя: `py -m unittest -v test_student.py`

### Шаг 3.1. Сначала просто посмотреть

```powershell
py step3_student.py
```

Не читайте пока код. Прочитайте **вывод**.

Функция `find_public` по замыслу должна показывать только записи с грифом «Открыто». Посмотрите на блок «Д3».

**Вопрос:** сколько записей с грифом «Конфиденциально» она вернула? Сколько должна была?

### Шаг 3.2. Разобраться, почему это произошло

Программа специально печатает получившийся SQL-запрос. Сравните два варианта:

```sql
-- обычный поиск
... WHERE label = 'Открыто' AND owner LIKE '%Иванова%'

-- поиск со строкой атаки
... WHERE label = 'Открыто' AND owner LIKE '%%' OR label LIKE '%%'
```

Подсказка: в SQL операция `AND` связывает сильнее, чем `OR`.

**Вопрос:** на какие два условия распалось второе выражение и почему одно из них истинно для всех строк?

### Шаг 3.3. Исправить Д3 — внедрение SQL-кода, CWE-89

Найдите в `find_public` строку, где запрос собирается склейкой. Исправьте.

Подсказка внутри самого файла: посмотрите, как написан вызов `executemany` в функции `connect` — там та же задача уже решена правильно.

**Проверка:** после исправления запустите файл ещё раз. В блоке Д3 должно быть `Утекло записей с грифом «Конфиденциально»: 0`.

### Шаг 3.4. Исправить Д4 — выход за каталог, CWE-22

Посмотрите на две последние строки вывода. Второй путь ведёт за пределы каталога `export/`.

Ваша задача — сделать так, чтобы имя файла с `..` было отклонено с ошибкой.

<details>
<summary>Подсказка первого уровня</summary>

Проверять исходную строку на наличие `..` ненадёжно: то же самое можно записать обратными слэшами, через подкаталог, через кодирование. Проверяйте не текст, а **результат**: во что путь превратился после приведения к каноническому виду.

</details>

<details>
<summary>Подсказка второго уровня</summary>

У объектов `Path` есть метод `.resolve()` — он убирает `..` и даёт полный путь. И метод `.is_relative_to(base)` — он отвечает, лежит ли путь внутри каталога `base`.

</details>

<details>
<summary>Подсказка третьего уровня — готовый блок</summary>

Этот приём разобран в конспекте, раздел 6, «Д4. Выход за пределы каталога». Здесь нужен не один символ, а целый блок из трёх строк — синтезировать его с нуля не требуется, разберите готовый:

```python
    base = EXPORT_DIR.resolve()
    candidate = (base / filename.replace("\\", "/")).resolve()
    if not candidate.is_relative_to(base):
        raise ValueError(f"Имя файла {filename!r} выводит за пределы каталога выгрузки")
```

Он заменяет строку с `os.path.join`. Дальше по коду вместо `path` используйте `candidate`.

**Разберите, что здесь происходит:** первая строка приводит к каноническому виду сам каталог выгрузки, вторая — путь вместе с именем от пользователя, третья — проверяет, что второй лежит внутри первого. Проверяется результат, а не текст.

</details>

### Шаг 3.5. Исправить Д1 и Д2 — секреты, CWE-798 и CWE-532

**Д1.** Токен написан прямо в коде. Уберите его оттуда: значение должно браться из переменной окружения `MEDIA_JOURNAL_ADMIN_TOKEN`.

Как задать переменную окружения в PowerShell:

```powershell
$env:MEDIA_JOURNAL_ADMIN_TOKEN = "demo-token-local-training"
```

> [!note] Здесь правится не одна строка, а три места
> Это самый «многошаговый» из четырёх дефектов, не пугайтесь:
> 1. Константа `ADMIN_TOKEN` вверху файла — её нужно убрать и вместо неё сделать функцию, читающую значение из окружения.
> 2. Сравнение в конце `login()` — оно ссылалось на константу, теперь должно вызывать эту функцию.
> 3. Функция `demo()` — там токен подставляется вторым, «спрятанным» способом. Найдите его: если его оставить, файл будет утверждать, что токена в коде нет, а токен там будет.
>
> Готовый разбор — в конспекте, раздел 6, «Д1. Секрет в исходном коде».

**Д2.** Токен попадает в журнал приложения. Сделайте так, чтобы вместо него писалась постоянная заглушка `***`.

> [!warning] Маска не должна ничего рассказывать о секрете
> Соблазнительно написать «красивую» маску вида `de***26 (длина 21)`. Так делать нельзя, и это распространённая ошибка. Длина отсекает большую часть вариантов перебора, а два известных символа с каждого конца — почти всё остальное. Маска обязана быть **одинаковой для любого секрета**: ни длины, ни первых символов, ни последних. Проверка `test_student.py` это контролирует.

**Вопрос по Д1:** если этот токен уже попал в репозиторий и его удалили следующим коммитом — считается ли он скомпрометированным?

<details>
<summary>Проверьте себя</summary>

Да. `git log -p` покажет содержимое старых коммитов. Единственное правильное действие после утечки секрета — **сменить сам секрет**, а не удалить строку. Чистка истории репозитория тоже нужна, но она вторична: пока старый токен действителен, он опасен.

</details>

### Шаг 3.6. Сверить с эталоном

Откройте `step3_fixed.py` и сравните со своим вариантом. Отличия — это нормально: важно, чтобы совпадал принцип, а не текст.

### Результат задания 3

- [ ] Все проверки `py -m unittest test_student.py` зелёные
- [ ] Д3 исправлен, строка атаки возвращает 0 конфиденциальных записей
- [ ] Д4 исправлен, имя файла с `..` отклоняется с ошибкой
- [ ] Д1 исправлен, токена в коде нет
- [ ] Д2 исправлен, в журнал идёт маска
- [ ] Названы все четыре идентификатора CWE
- [ ] Сделан коммит с осмысленным сообщением

---

## Задание 4. Тесты, проверки, релиз

**Пара 5, 16:50–18:20.**
**Цель:** написать тест, который не даст уязвимости вернуться, и увидеть разницу между инструментами проверки.

> [!important] Что обязательно, а что по возможности
> **Обязательно** — шаги 4.1–4.5. Это ядро пары, его хватает для результата.
> **По возможности** — шаги 4.6–4.9 (bandit, конвейер, тег, паспорт). Если времени не осталось, разбираем их вместе с преподавателем на экране или оставляем на самостоятельную работу. Ничего не потеряете: обязательная часть самодостаточна.

### Шаг 4.1. Посмотреть, как выглядит зелёный результат

```powershell
py -m unittest -v test_journal.py
```

**Ожидаемый результат:** `Ran 19 tests ... OK`

Эти тесты проверяют **эталон** — `step3_fixed.py`. Так выглядит правильно решённая задача.

### Шаг 4.2. Проверить СВОЮ работу

А эти — ваш файл `step3_student.py`:

```powershell
py -m unittest -v test_student.py
```

**Если задание 3 сделано** — всё зелёное, `OK`. Поздравляю, это и есть подтверждение, что вы починили именно то, что нужно, и ничего не сломали попутно.

**Если ещё не сделано** — часть проверок красная, по одной на каждый дефект. Это тоже полезный результат: посмотрите, что именно пишет каждая упавшая проверка.

### Шаг 4.3. Убедиться, что тесты умеют краснеть

Тест, который никогда не падал, ничего не доказывает. Проверим это на живом примере — временно верните свой файл в исходное дефектное состояние:

```powershell
copy step3_student.py step3_student_moya_rabota.py
copy step3_defect.py step3_student.py
py -m unittest test_student.py
```

**Ожидаемый результат:** проверки стали красными, и среди них видны нарушения по каждому из четырёх дефектов. Точное количество отказов запоминать не нужно — оно меняется, как только вы добавите свои тесты. Важно другое: проверки реагируют на дефекты, а не просто «всегда зелёные».

Верните свою работу обратно:

```powershell
copy step3_student_moya_rabota.py step3_student.py
py -m unittest test_student.py
```

Снова зелено. Вот теперь тестам можно верить.

### Шаг 4.4. Написать свои два теста

Добавьте в класс `TestSecurityRegressions` два своих теста:

1. Функциональный: проверить, что поиск по несуществующему владельцу возвращает пустой список.
2. Регрессионный: взять любое из пяти требований «никогда», которые мы сформулировали утром на доске, и превратить его в тест.

<details>
<summary>Образец, от которого можно оттолкнуться</summary>

Добавьте в класс `TestSecurityRegressions` файла `test_journal.py`. Обратите внимание на `self.module` — благодаря ему ваш тест автоматически применится и к эталону, и к вашему файлу:

```python
    def test_search_for_unknown_owner_returns_nothing(self) -> None:
        rows = self.module.find_public(self.connection, "Несуществующая")
        self.assertEqual(rows, [])
```

Проверьте обеими командами: `py -m unittest test_journal.py` и `py -m unittest test_student.py`.

</details>

### Шаг 4.5. Линтер против анализатора безопасности

```powershell
py -m ruff check step3_defect.py
```

**Ожидаемый результат:** ruff найдёт **одну** проблему — неиспользуемую переменную `lines`.

**Вопрос:** а SQL-инъекцию он нашёл? Почему?

---

*Дальше — по возможности. Если время вышло, разбираем вместе или дома.*

### Шаг 4.6. Анализатор безопасности

Если на машине установлен bandit:

```powershell
py -X utf8 -m bandit step3_defect.py
```

Ключ `-X utf8` обязателен: без него на обычной Windows-консоли команда упадёт на кириллице ещё до того, как покажет находки.

Он должен сработать по двум правилам: **B105** — секрет в коде, **B608** — SQL, собранный склейкой.

Заполните таблицу:

| Инструмент | Что ищет | Нашёл ли Д3 |
|---|---|---|
| Тесты | | |
| ruff | | |
| bandit | | |
| pip-audit | | |

### Шаг 4.7. Конвейер

Откройте файл `.gitlab-ci.yml` в каталоге `code/`. Найдите в нём те самые команды, которые вы вводили руками.

**Вопрос:** какой из шагов конвейера обязан останавливать сборку при отрицательном результате, а какой может только предупреждать?

### Шаг 4.8. Релиз

```powershell
git add .
git commit -m "Исправлены четыре дефекта безопасности, добавлены тесты"
git tag v1.0
git log --oneline
```

### Шаг 4.9. Мини-паспорт РБПО

Заполните вместе с преподавателем.

| Поле | Ваш ответ |
|---|---|
| Наименование и версия | |
| Язык и среда выполнения | |
| Правила кодирования | |
| Контроль ввода | |
| Работа с секретами | |
| Тестирование (сколько тестов, из них по безопасности) | |
| Статический анализ | |
| Управление конфигурацией | |
| **Известные ограничения — чего в продукте НЕТ** | |

Последняя строка обязательна. Честный перечень того, чего в продукте нет, — признак зрелого процесса, а не слабости.

### Результат задания 4

- [ ] 19 готовых тестов проходят
- [ ] Тесты проверены на дефектной версии — они падают
- [ ] Написаны два своих теста
- [ ] Запущен ruff, есть ответ, почему он не нашёл инъекцию
- [ ] Заполнена таблица инструментов
- [ ] Поставлен тег `v1.0`
- [ ] Заполнен мини-паспорт

---

## Задание 5 «со звёздочкой» — если осталось время

В файле `step3_defect.py` (в образце, не в вашем) есть **пятая** проблема, о которой нигде не сказано и которая не помечена комментарием. Она не относится к безопасности напрямую, но линтер её находит.

Найдите её без подсказок. Объясните, почему она — не уязвимость, но всё равно дефект.

---

## Бумажный чек-лист правил кодирования

Работает без компьютера. Если инструменты недоступны — проверка по этому списку заменяет запуск SAST.

Проходите по своему коду и отвечайте «да» или «нет» на каждый пункт.

**Данные, приходящие снаружи**
- [ ] Каждое значение, пришедшее от пользователя, проверено до использования
- [ ] Проверка сделана по белому списку разрешённого, а не по чёрному списку запрещённого
- [ ] Проверка встроена в структуру данных, а не выполняется в одном месте программы

**Команды и данные**
- [ ] Ни один SQL-запрос не собирается склейкой строк
- [ ] Ни одна команда операционной системы не собирается склейкой строк
- [ ] Ни один путь к файлу не строится из пользовательского ввода без нормализации и проверки границы

**Секреты**
- [ ] В исходном коде нет ни паролей, ни токенов, ни ключей
- [ ] Секреты не пишутся в журналы приложения
- [ ] Файлы с секретами перечислены в `.gitignore`
- [ ] `.gitignore` создан до первого коммита

**Ошибки и журналы**
- [ ] Программа при неверных данных останавливается, а не продолжает работу в неизвестном состоянии
- [ ] Сообщение об ошибке не раскрывает наружу внутреннее устройство системы
- [ ] В журнале нет персональных данных и содержимого учётных записей

**Проверяемость**
- [ ] Каждое требование безопасности, сформулированное через «никогда», имеет свой тест
- [ ] Каждый исправленный дефект получил регрессионный тест
- [ ] Проверено, что тесты падают на дефектной версии кода
