diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d3bb44554 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + pull_request: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + +permissions: + contents: read + +jobs: + vet: + name: vet (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go vet + working-directory: app + run: go vet ./... + + test: + name: test (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go test -race + working-directory: app + run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: '1.24' + cache: true + cache-dependency-path: app/go.sum + + - name: Install golangci-lint v2.5.0 + working-directory: app + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + + - name: Run golangci-lint + working-directory: app + run: golangci-lint run + + ci-ok: + name: ci-ok + if: always() + needs: [ vet, test, lint ] + runs-on: ubuntu-24.04 + steps: + - name: Check results + run: | + if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then + echo "Some jobs failed or were cancelled" + exit 1 + fi + echo "All jobs passed" \ No newline at end of file diff --git a/.github/workflows/ci.yml.backup b/.github/workflows/ci.yml.backup new file mode 100644 index 000000000..d3bb44554 --- /dev/null +++ b/.github/workflows/ci.yml.backup @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + pull_request: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + +permissions: + contents: read + +jobs: + vet: + name: vet (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go vet + working-directory: app + run: go vet ./... + + test: + name: test (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go test -race + working-directory: app + run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: '1.24' + cache: true + cache-dependency-path: app/go.sum + + - name: Install golangci-lint v2.5.0 + working-directory: app + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + + - name: Run golangci-lint + working-directory: app + run: golangci-lint run + + ci-ok: + name: ci-ok + if: always() + needs: [ vet, test, lint ] + runs-on: ubuntu-24.04 + steps: + - name: Check results + run: | + if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then + echo "Some jobs failed or were cancelled" + exit 1 + fi + echo "All jobs passed" \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..eacad3b81 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,36 @@ +# ===== СТЕЙДЖ 1: СБОРКА ===== +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +# Кешируем зависимости +COPY go.mod go.sum ./ +RUN go mod download + +# Копируем исходники +COPY . . + +# Собираем статический бинарник +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags='-s -w' \ + -trimpath \ + -o quicknotes . + +# ===== СТЕЙДЖ 2: РАНТАЙМ ===== +FROM gcr.io/distroless/static:nonroot + +WORKDIR /app + +COPY --from=builder /build/quicknotes . + +COPY --from=busybox:stable-musl /bin/busybox /bin/busybox + +# Создаём каталог /data и даём права пользователю 65532 +USER root +RUN ["/bin/busybox", "mkdir", "-p", "/data"] +RUN ["/bin/busybox", "chown", "65532:65532", "/data"] +USER 65532 + +EXPOSE 8080 + +ENTRYPOINT ["/app/quicknotes"] \ No newline at end of file diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 000000000..e69de29bb diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..87512cc97 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,35 @@ +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + - ADDR=:8080 + - DATA_PATH=/data/notes.json + - SEED_PATH=/data/seed.json # если seed.json не нужен, оставь как есть + healthcheck: + test: ["CMD", "/bin/busybox", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + + # Бонусные параметры безопасности (все 6) + user: "65532:65532" + read_only: true + tmpfs: + - /tmp + - /run + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + +volumes: + quicknotes-data: \ No newline at end of file diff --git a/submissions/PASS.png b/submissions/PASS.png new file mode 100644 index 000000000..f51de156e Binary files /dev/null and b/submissions/PASS.png differ diff --git a/submissions/branch.png b/submissions/branch.png new file mode 100644 index 000000000..4820f11fa Binary files /dev/null and b/submissions/branch.png differ diff --git a/submissions/fail.png b/submissions/fail.png new file mode 100644 index 000000000..4367dad25 Binary files /dev/null and b/submissions/fail.png differ diff --git a/submissions/image.png b/submissions/image.png new file mode 100644 index 000000000..73e08acf8 Binary files /dev/null and b/submissions/image.png differ diff --git a/submissions/image1.png b/submissions/image1.png new file mode 100644 index 000000000..ebab70ba4 Binary files /dev/null and b/submissions/image1.png differ diff --git a/submissions/image10.png b/submissions/image10.png new file mode 100644 index 000000000..de685b086 Binary files /dev/null and b/submissions/image10.png differ diff --git a/submissions/image11.png b/submissions/image11.png new file mode 100644 index 000000000..d0c4a812b Binary files /dev/null and b/submissions/image11.png differ diff --git a/submissions/image12.png b/submissions/image12.png new file mode 100644 index 000000000..eea3dd9c9 Binary files /dev/null and b/submissions/image12.png differ diff --git a/submissions/image2.png b/submissions/image2.png new file mode 100644 index 000000000..44b400579 Binary files /dev/null and b/submissions/image2.png differ diff --git a/submissions/image3.png b/submissions/image3.png new file mode 100644 index 000000000..b674b8aa7 Binary files /dev/null and b/submissions/image3.png differ diff --git a/submissions/image4.png b/submissions/image4.png new file mode 100644 index 000000000..f06e763f5 Binary files /dev/null and b/submissions/image4.png differ diff --git a/submissions/image5.png b/submissions/image5.png new file mode 100644 index 000000000..4a471b21d Binary files /dev/null and b/submissions/image5.png differ diff --git a/submissions/image6.png b/submissions/image6.png new file mode 100644 index 000000000..f64d1fe6f Binary files /dev/null and b/submissions/image6.png differ diff --git a/submissions/image7.png b/submissions/image7.png new file mode 100644 index 000000000..7f5558454 Binary files /dev/null and b/submissions/image7.png differ diff --git a/submissions/image8.png b/submissions/image8.png new file mode 100644 index 000000000..49ca7da81 Binary files /dev/null and b/submissions/image8.png differ diff --git a/submissions/image9.png b/submissions/image9.png new file mode 100644 index 000000000..ea4fa4f0c Binary files /dev/null and b/submissions/image9.png differ diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..bb2e355db --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,63 @@ +# Lab 3 — CI/CD: A PR-Gated Pipeline for QuickNotes + +**Студент:** Руслан Кудинов +**Путь:** GitHub Actions +**Дата:** 17.06.2026 + +## Выбранный путь +Я выбрал GitHub Actions, так как это стандартный инструмент для курса. Из-за проблем с биллингом на основном аккаунте пришлось создать новый (RusKudinov). + +## Ссылка на зелёный CI run +https://github.com/RusKudinov/DevOps-Intro/actions/runs/27708444304 + +## Доказательство работы гейта +- **Сломанный тест:** ![альтернативный текст](fail.png) +- **Исправление:** ![альтернативный текст](PASS.png) +- **Коммит с поломкой:** `3b6b086 ` (можно указать) +- **Коммит с исправлением:** `014941a` + +## Скриншот branch protection +![альтернативный текст](branch.png) +--- + +## Ответы на дизайн-вопросы (Task 1.2) + +### a) Почему пиннить `ubuntu-24.04`, а не `ubuntu-latest`? +`ubuntu-latest` — плавающий тег, который может измениться (например, перейти на 26.04). Это приведёт к непредсказуемым изменениям окружения (версия ядра, библиотеки). Пиннинг фиксирует конкретную версию, делая сборку воспроизводимой. + +### b) Почему разделить vet, test, lint на отдельные джобы? +Если объединить их в одну, при падении lint мы не узнаем, прошли ли тесты. Разделение даёт параллельность, независимый статус каждой проверки и возможность использовать матрицу только для vet и test. + +### c) Какую атаку предотвращает SHA‑пиннинг? (GH path) +В марте 2025 года был скомпрометирован экшен `tj-actions/changed-files`. Злоумышленник внёс вредоносный код в тег `v4`. Все, кто использовал `@v4`, автоматически подхватили его. Пиннинг по SHA фиксирует конкретный коммит, который мы проверили, и защищает от таких supply‑chain‑атак. + +### d) Что такое `permissions:` и какой принцип? +`permissions:` определяет уровень доступа токена GITHUB_TOKEN в workflow. Мы устанавливаем `contents: read` — только чтение кода. Это принцип наименьших привилегий: даём минимум прав, необходимых для работы. Снижает ущерб при компрометации. + +--- + +## Ответы на вопросы Task 2 + +### f) Почему кэшировать по `go.sum`, а не по build‑output? +`go.sum` содержит хеши зависимостей — это надёжный идентификатор набора модулей. Build‑output зависит от архитектуры, версии Go, флагов сборки и может меняться без изменения кода. Кэширование по `go.sum` гарантирует, что при одинаковых входных данных кэш подходит. + +### g) Что делает `fail-fast: false` и когда нужен `true`? +`fail-fast: false` в матрице позволяет продолжать выполнение остальных комбинаций, даже если одна упала. Так мы видим все ошибки (например, тесты падают только на Go 1.24). `fail-fast: true` (по умолчанию) останавливает всё при первом падении — ускоряет фидбек, если ошибка, скорее всего, общая. + +### h) Какой риск кэша, созданного вредоносным PR? +Злоумышленник может попытаться записать кэш с вредоносными зависимостями. Однако GitHub не позволяет PR из форка читать кэш основной ветки и не даёт записывать кэш, который будет использован в `main`. Кэш привязан к ветке и SHA. Это задокументировано в официальной документации GitHub. + +--- + +## Таблица времени (Task 2.4) +![alt text](test1.png) +![alt text](test2.png) +![alt text](test3.png) +| Сценарий | Wall‑clock (сек) | +|----------|------------------| +| Без кэша, одна версия Go, без path‑фильтра | 81 | +| С кэшем (один Go) | 70 | +| С кэшем + матрица (две версии) | 80 | + +**Замечание:** В QuickNotes нет внешних зависимостей, поэтому кэш почти не даёт выигрыша. Основное время уходит на установку Go и старт раннера. В реальном проекте с сотнями модулей выгода была бы значительной. + diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..67d6297bc --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,332 @@ +# Lab 6 — Containers: Dockerize QuickNotes + +## Выполнил: Кудинов Рулсан +## Дата: 24.06.2026 + +--- + +## 1. Dockerfile (файл `app/Dockerfile`) + +```dockerfile +# ===== СТЕЙДЖ 1: СБОРКА ===== +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +# Кешируем зависимости +COPY go.mod go.sum ./ +RUN go mod download + +# Копируем исходники +COPY . . + +# Собираем статический бинарник +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags='-s -w' \ + -trimpath \ + -o quicknotes . + +# ===== СТЕЙДЖ 2: РАНТАЙМ ===== +FROM gcr.io/distroless/static:nonroot + +WORKDIR /app + +COPY --from=builder /build/quicknotes . + +COPY --from=busybox:stable-musl /bin/busybox /bin/busybox + +# Создаём каталог /data с правами nonroot +USER root +RUN ["/bin/busybox", "mkdir", "-p", "/data"] +RUN ["/bin/busybox", "chown", "65532:65532", "/data"] +USER 65532 + +EXPOSE 8080 + +ENTRYPOINT ["/app/quicknotes"] +``` + +--- + +## 2. Compose-файл (`compose.yaml`) + +```yaml +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + - ADDR=:8080 + - DATA_PATH=/data/notes.json + - SEED_PATH=/data/seed.json + healthcheck: + test: ["CMD", "/bin/busybox", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + + # Security hardening (6 defaults) + user: "65532:65532" + read_only: true + tmpfs: + - /tmp + - /run + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + +volumes: + quicknotes-data: +``` + +--- + +## 3. Проверка размера образа + +**Команда:** +```powershell +docker images quicknotes:lab6 +``` + +**Скриншот 1** — ![](image1.png) + +**Вывод (текстовый):** +``` +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +quicknotes:lab6 9dd77b1e8c59 16.8MB 4.09MB U +``` +Размер: **16.8 МБ** — условие выполнено. + +--- + +## 4. Проверка работы приложения (эндпоинт /health) + +**Команда:** +```powershell +curl.exe http://localhost:8080/health +``` + +**Скриншот 2** ![](image2.png) + +**Вывод:** +```json +{"notes":0,"status":"ok"} +``` + +--- + +## 5. Тест сохранения данных (persistence) + +### 5.1 Создание заметки и проверка наличия + +**Команда:** +```powershell +$body = '{"title":"durable","body":"survive a restart"}' +Invoke-WebRequest -Uri http://localhost:8080/notes -Method POST -Body $body -ContentType "application/json" +``` + +**Скриншот 3** — ![скриншот с ответом 201 Created и содержимым заметки.](image3.png) + +**Команда проверки:** +```powershell +curl.exe -s http://localhost:8080/notes +``` +**Скриншот 4** — ![скриншот с выводом JSON-массива, содержащего созданную заметку.](image4.png) + +**Вывод:** +```json +[{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-24T17:34:50.077826506Z"}] +``` + +### 5.2 Перезапуск контейнера (том сохраняется) + +```powershell +docker compose down +docker compose up -d +curl.exe -s http://localhost:8080/notes +``` + +**Скриншот 5** — ![скриншот, где после `docker compose up -d` команда `curl` снова показывает ту же заметку.](image5.png) + +**Вывод:** +```json +[{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-24T17:34:50.077826506Z"}] +``` + +### 5.3 Удаление тома и проверка + +```powershell +docker compose down -v +docker compose up -d +curl.exe -s http://localhost:8080/notes +``` + +**Скриншот 6** — ![скриншот, где после `docker compose down -v` и повторного подъёма команда `curl` возвращает `\[\]`.](image6.png) + +**Вывод:** +```json +[] +``` + +**Вывод:** данные сохраняются после перезапуска и исчезают только при удалении тома — persistence работает. + +--- + +## 6. Проверка security-настроек (бонус) + +Все команды выполнялись из корня проекта. + +### 6.1 Пользователь nonroot + +**Команда:** +```powershell +docker inspect quicknotes:lab6 --format '{{ .Config.User }}' +``` + +**Скриншот 7** — ![вывод `65532`](image7.png) + +**Вывод:** +``` +65532 +``` + +### 6.2 Отсутствие шелла + +**Команда:** +```powershell +docker compose exec quicknotes sh +``` + +**Скриншот 8** — ![ошибка `exec: "sh": executable file not found in $PATH`.](image8.png) + +**Вывод:** +``` +OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH +``` + +### 6.3 Сброс capabilities + +**Команда:** +```powershell +docker inspect $(docker compose ps -q quicknotes) --format '{{ .HostConfig.CapDrop }}' +``` + +**Скриншот 9** — ![вывод `\[ALL\]`.](image9.png) + +**Вывод:** +``` +[ALL] +``` + +### 6.4 Read-only root + +**Команда:** +```powershell +docker compose exec quicknotes /bin/busybox touch /etc/test 2>&1 +``` + +**Скриншот 10** — ![ошибка `touch: /etc/test: Read-only file system`.](image10.png) + +**Вывод:** +``` +touch: /etc/test: Read-only file system +``` + +### 6.5 no-new-privileges + +**Команда:** +```powershell +docker inspect $(docker compose ps -q quicknotes) --format '{{ .HostConfig.SecurityOpt }}' +``` + +**Скриншот 11** — ![вывод `\[no-new-privileges:true\]`.](image11.png) + +**Вывод:** +``` +[no-new-privileges:true] +``` + +### 6.6 Trivy сканирование + +**Команда:** +```powershell +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 +``` + +**Скриншот 12** — ![полный вывод Trivy (или хотя бы итоговая таблица).](image12.png) +![alt text](image.png) +**Сокращённый вывод:** +``` +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +app/quicknotes (gobinary) +========================= +Total: 13 (HIGH: 13, CRITICAL: 0) +... +``` +На уровне ОС уязвимостей нет, в Go-библиотеке stdlib обнаружены 13 HIGH, но они исправлены в более новых версиях Go. + +--- + +## 7. Ответы на вопросы + +### a) Почему порядок слоёв важен? + +Порядок инструкций в Dockerfile влияет на использование кеша слоёв. Сначала копируются только `go.mod` и `go.sum`, затем выполняется `go mod download`. Этот слой пересобирается только при изменении зависимостей. Если сначала скопировать весь код, то любое изменение в исходниках приведёт к повторной загрузке всех зависимостей, что замедляет сборку. + +### b) Зачем `CGO_ENABLED=0`? + +Отключает использование C-кода и динамическую линковку. Бинарник становится полностью статическим и не требует внешних библиотек. В образе `distroless/static` нет динамического линковщика, поэтому без этого флага запуск невозможен (ошибка `no such file or directory`). + +### c) Что такое `gcr.io/distroless/static:nonroot`? + +Это минимальный образ от Google, содержащий только статически скомпилированный бинарник и минимальные системные файлы (например, `ca-certificates`, `timezone`). В нём нет оболочки, пакетного менеджера, утилит. Это снижает поверхность атак и количество уязвимостей. + +### d) `-ldflags='-s -w'` и `-trimpath` + +- `-s` — удаляет таблицу символов. +- `-w` — удаляет отладочную информацию (DWARF). +Эти флаги уменьшают размер бинарника. +- `-trimpath` — убирает абсолютные пути к исходникам, делая сборку воспроизводимой. +Цена — потеря отладочных символов, что затрудняет отладку в продакшене, но для финального образа это приемлемо. + +### e) Как сделать healthcheck без шелла? + +Мы скопировали статический `busybox` в образ и используем его команду `wget` для проверки `/health`. Поскольку `busybox` собран как статический бинарник, он работает без динамических библиотек и не требует оболочки. + +### f) Почему том сохраняется после `docker compose down`? + +Именованный том (`quicknotes-data`) управляется Docker отдельно от контейнера. Команда `docker compose down` без флага `-v` удаляет только контейнеры и сеть, но не тома. При следующем `up` том подключается с теми же данными. + +### g) `depends_on` без `condition: service_healthy` + +`depends_on` без условия `service_healthy` дожидается только запуска контейнера (статус `running`), но не проверяет, что сервис внутри готов принимать запросы. Это может привести к ошибкам, если зависимый сервис ещё не инициализировался. + +--- + +## 8. Бонус: какая из 6 мер даёт больше всего безопасности на строчку YAML? + +Самый большой эффект дают `cap_drop: ALL` и `read_only: true`. Они кардинально ограничивают возможности контейнера даже в случае взлома приложения, практически не увеличивая сложность конфигурации. + +--- + +## Заключение + +Все требования Lab 6 выполнены: +- Многостадийный Dockerfile собран, размер образа ≤ 25 МБ. +- Compose-файл с томом, healthcheck и security-параметрами работает. +- Persistence подтверждена. +- Все 6 security defaults применены и верифицированы. +- Trivy запущен, результаты задокументированы. + diff --git a/submissions/test1.png b/submissions/test1.png new file mode 100644 index 000000000..469c86d28 Binary files /dev/null and b/submissions/test1.png differ diff --git a/submissions/test2.png b/submissions/test2.png new file mode 100644 index 000000000..dd3986d60 Binary files /dev/null and b/submissions/test2.png differ diff --git a/submissions/test3.png b/submissions/test3.png new file mode 100644 index 000000000..f384f8687 Binary files /dev/null and b/submissions/test3.png differ