Skip to content
Open

lab9 #1407

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM golang:1.24-alpine AS builder
WORKDIR /build
COPY app/go.mod ./
RUN go mod download
COPY app/ .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -trimpath \
-o /build/quicknotes .

# Создаём папку /data и даём права в builder
RUN mkdir -p /data && chown 65532:65532 /data

FROM gcr.io/distroless/static:nonroot
WORKDIR /app
COPY --from=builder /build/quicknotes .
COPY --from=builder /data /data
COPY app/seed.json .
EXPOSE 8080
USER 65532:65532
ENTRYPOINT ["/app/quicknotes"]
20 changes: 19 additions & 1 deletion app/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ func do(t *testing.T, srv *Server, method, target string, body any) *httptest.Re
}
req := httptest.NewRequest(method, target, &buf)
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
handler := securityHeaders(srv.Routes())
handler.ServeHTTP(rec, req)
return rec
}

Expand Down Expand Up @@ -131,3 +132,20 @@ func TestMetrics_ExposesPrometheusFormat(t *testing.T) {
}
}

func TestSecurityHeaders(t *testing.T) {
srv := newTestServer(t)
rec := do(t, srv, http.MethodGet, "/health", nil)

if rec.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Errorf("X-Content-Type-Options header missing or wrong: %q", rec.Header().Get("X-Content-Type-Options"))
}
if rec.Header().Get("X-Frame-Options") != "DENY" {
t.Errorf("X-Frame-Options header missing or wrong: %q", rec.Header().Get("X-Frame-Options"))
}
if rec.Header().Get("Content-Security-Policy") != "default-src 'none'" {
t.Errorf("Content-Security-Policy header missing or wrong: %q", rec.Header().Get("Content-Security-Policy"))
}
if rec.Header().Get("Referrer-Policy") != "no-referrer" {
t.Errorf("Referrer-Policy header missing or wrong: %q", rec.Header().Get("Referrer-Policy"))
}
}
4 changes: 3 additions & 1 deletion app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ func main() {
}

server := NewServer(store)
handler := securityHeaders(server.Routes())

srv := &http.Server{
Addr: addr,
Handler: server.Routes(),
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
}

Expand Down
13 changes: 13 additions & 0 deletions app/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import "net/http"

func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'none'")
w.Header().Set("Referrer-Policy", "no-referrer")
next.ServeHTTP(w, r)
})
}
26 changes: 26 additions & 0 deletions app/zap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
env:
contexts:
- excludePaths: []
name: baseline
urls:
- http://host.docker.internal:8080
parameters:
failOnError: true
progressToStdout: false
jobs:
- parameters:
enableTags: false
maxAlertsPerRule: 10
type: passiveScan-config
- parameters:
maxDuration: 1
url: http://host.docker.internal:8080
type: spider
- parameters:
maxDuration: 0
type: passiveScan-wait
- parameters:
format: Long
summaryFile: /home/zap/zap_out.json
rules: []
type: outputSummary
35 changes: 35 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
quicknotes:
image: quicknotes:lab6
container_name: quicknotes-lab6
restart: unless-stopped
user: "0:0"
ports:
- "8080:8080"
volumes:
- quicknotes-data:/data
environment:
- ADDR=:8080
- DATA_PATH=/data/notes.json
- SEED_PATH=/app/seed.json
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s

# Drop all capabilities
cap_drop:
- ALL
# добавлять ничего не надо, так как приложению не нужны capabilities

# Read-only root filesystem
read_only: true

# no-new-privileges
security_opt:
- no-new-privileges:true

volumes:
quicknotes-data:
Binary file added submissions/image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image-6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image-7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added submissions/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
157 changes: 157 additions & 0 deletions submissions/lab9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Lab 9

Frolova AI - M25RO-01,

a.frolova@innopolis.university

Ссылка на PR: https://github.com/inno-devops-labs/DevOps-Intro/pull/1407

## 1.1 Необходимые сканы

1. Сканиерование образа

```bash
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 > submissions/trivy-image.txt
```

Скачалась база уязвимостей, определилась ОС, нашелся Go-бинарник, завершилось сканирование, и вывод ушел в файл `submissions/trivy-image.txt`.

![alt text](image.png)

Файл `submissions/trivy-image.txt`:

![alt text](image-1.png)

2. Сканирование файловой системы репозитория

```bash
docker run --rm -v //var/run/docker.sock:/var/run/docker.sock -v "$PWD:/src" aquasec/trivy:0.59.1 fs --severity HIGH,CRITICAL --no-progress /src > submissions/trivy-fs.txt
```

Здесь слетела кодировка, но речь идет о том, что найдена 1 уязвимость HIGH, AsymmetricPrivateKey - это приватный ssh ключ, который попал в репозиторий, и указано, по какому пути находится файл.

Файл `trivy-fs`:

![alt text](image-2.png)

Эту проблему можно решить, добвив .vagrant в gitignore и удалив файл из репозитория.

3. Сканирование конфигурации

```bash
docker run --rm -v //var/run/docker.sock:/var/run/docker.sock -v "${PWD}:/src" aquasec/trivy:0.59.1 config --severity HIGH,CRITICAL /src 2>&1 | Out-File -Encoding UTF8 submissions/trivy-config.txt
```

Файл `trivy-config`:

![alt text](image-3.png)

Была найдена одна уязвимость Dockerfile - отсутвует user инструкция, контейнер запускается от root. Нужно изменить Dockerfile и добавить `USER 65532:65532`.

4. Генерация SBOM

```bash
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.59.1 image --format cyclonedx quicknotes:lab6 > submissions/sbom.json
```

Фрагмент файла `sbom`:

![alt text](image-4.png)

Здесь содержится информация об ОС (Debian 13.5), базовых пактах, go приложении и зависимостях.


## Ответы на вопросы

a) CVE severity is one input, not the answer. What else matters when triaging?

Кроме CVSS-оценки важны:

- **Reachability** - вызывается ли уязвимая функция в нашем коде? Если нет, то риск значительно ниже.
- **Exploit availability** - есть ли публичный PoC/эксплойт? Если есть, приоритет выше.
- **Deployment context** - изолирован ли сервис (например, в закрытой сети) или доступен из интернета?
- **Business impact** - что именно ломается: критичный API или вспомогательный эндпоинт?

Только совокупность этих факторов позволяет принять взвешенное решение: фиксить, принять риск или отложить.

---

b) Why is the minimal base the strongest single security control?

Минимальный базовый образ (distroless, scratch) содержит только статически собранный бинарник и минимальные файлы. Это резко уменьшает поверхность атаки:
- Нет shell, пакетного менеджера, компилятора, утилит
- Практически нет CVE в самом образе
- Нечем злоупотреблять при компрометации контейнера

Это единственный контроль, который одномоментно устраняет сотни потенциальных уязвимостей.

---

c) `.trivyignore` — when is it the right move, and when is it security theater?

**Правильно:** когда уязвимость точно не эксплуатируется в данном контексте (например, CVE в библиотеке, которая не используется в коде), и к этому приложена дата пересмотра.

**Театр:** когда игнорируют находки без анализа, «чтобы отчёт был зелёным». Если нет даты пересмотра или причины — это просто заметание проблем под ковёр.

---

d) The SBOM is a list of components. What concrete future problem does having it today solve?

SBOM позволяет **мгновенно ответить на вопрос**: «Затронут ли нас новый CVE?»

Например, когда вышла Log4Shell (2021), компании с SBOM смогли за минуты проверить, используется ли Log4j в их проектах. Без SBOM — приходилось вручную проверять каждый репозиторий, что занимало дни и недели.

## Task2

### 2.1 ZAP baseline

```bash
docker run --rm -v "$(pwd -W):/zap/wrk" ghcr.io/zaproxy/zaproxy:2.16.0 zap-baseline.py -t http://host.docker.internal:8080 2>&1 | tee submissions/zap-report.txt

```

Фрагмент файла `zap-report`:

![alt text](image-5.png)

Zap нашел 3 url для сканирования, провел 65 пассивных проверок, которые успешно прошли. Найдено 2 предупреждения (2 url не существует).

ZAP предупреждает, что версия устарела. Критических уязвимостей не найдено.

### 2.2 ZAP findings triage

| ID | Name | Risk | URL | Disposition | Reason |
|----|------|------|-----|-------------|--------|
| 10049 | Storable and Cacheable Content | Medium | `/` | FIX | Добавить Cache-Control заголовки |
| 10049 | Storable and Cacheable Content | Medium | `/sitemap.xml` | ACCEPT | QuickNotes — API, sitemap.xml не существует |
| 10116 | ZAP is Out of Date | Informational | — | ACCEPT | Используется фиксированная версия ZAP 2.16.0 |

### 2.3

- Добавлен middleware `securityHeaders` в `app/middleware.go`, который устанавливает заголовки безопасности (`X-Content-Type-Options`, `X-Frame-Options`, `CSP`, `Referrer-Policy`).
- Middleware подключён ко всем маршрутам в `main.go`.
- Добавлен тест `TestSecurityHeaders`, проверяющий наличие заголовков.
- Тест проходит успешно (`PASS`), заголовки применяются корректно.

![alt text](image-6.png)

### 2.4 Повторное сканирование

Результат:

![alt text](image-7.png)

### Ответы на вопросы

e) Why a middleware and not per-handler header sets?

Middleware применяется ко всем маршрутам сразу, не дублирует код, легче поддерживать и изменять. Это соответствует принципу DRY (Don't Repeat Yourself).
---
f) `Content-Security-Policy: default-src 'none'` is the strictest CSP. What does it break? Why is it OK for QuickNotes (an API) but not for a website?

CSP `default-src 'none'` запрещает любые внешние ресурсы (скрипты, стили, изображения, шрифты). Для API это безопасно, потому что API не загружает внешние ресурсы. Для веб-сайта это сломает рендеринг страниц, так как браузер не сможет загрузить CSS, JS, шрифты и т.д.
---

g) False positives vs accepted findings: ZAP often flags informational issues that aren't real problems. What's the cost of marking them all "accepted" without reading them?

Если принимать все находки без проверки, можно пропустить реальную уязвимость, замаскированную под ложную. Это создаёт иллюзию безопасности («мы всё проверили»), но на деле оставляет дыры. Каждое принятие должно быть осознанным и задокументированным.
Binary file added submissions/sbom.json
Binary file not shown.
53 changes: 53 additions & 0 deletions submissions/trivy-config.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
docker : 2026-07-08T17:39:46Z INFO [misconfig] Misconfiguration scanning is enabled
строка:1 знак:1
+ docker run --rm -v //var/run/docker.sock:/var/run/docker.sock -v "${P ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (2026-07-08T17:3...ning is enabled:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError

2026-07-08T17:39:46Z INFO [misconfig] Need to update the built-in checks
2026-07-08T17:39:46Z INFO [misconfig] Downloading the built-in checks...
165.46 KiB / 165.46 KiB [------------------------------------------------------] 100.00% ? p/s 100ms2026-07-08T17:39:49
Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.cache/trivy/policy/c
ontent/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/po
licies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\
tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t
want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnet
s\" \"volumes\" \"vpcs\"]"
2026-07-08T17:39:49Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/p
olicies/cloud/policies/aws/ec2/specify_ami_owners.rego"
2026-07-08T17:39:49Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/clo
ud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/spec
ify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requ
estedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"
instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"v
pcs\"]"
2026-07-08T17:39:50Z ERROR [rego] Error occurred while parsing. Trying to fallback to embedded check file_path="root/.c
ache/trivy/policy/content/policies/cloud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/conten
t/policies/cloud/policies/aws/ec2/specify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedam
is[__local622__]\n\tinput.aws.ec2.requestedamis[__local622__]\n\t ^\n\t have: \"requestedamis
\"\n\t want (one of): [\"instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securi
tygroups\" \"subnets\" \"volumes\" \"vpcs\"]"
2026-07-08T17:39:50Z ERROR [rego] Failed to find embedded check, skipping file_path="root/.cache/trivy/policy/content/p
olicies/cloud/policies/aws/ec2/specify_ami_owners.rego"
2026-07-08T17:39:50Z ERROR [rego] Error occurred while parsing file_path="root/.cache/trivy/policy/content/policies/clo
ud/policies/aws/ec2/specify_ami_owners.rego" err="root/.cache/trivy/policy/content/policies/cloud/policies/aws/ec2/spec
ify_ami_owners.rego:30: rego_type_error: undefined ref: input.aws.ec2.requestedamis[__local622__]\n\tinput.aws.ec2.requ
estedamis[__local622__]\n\t ^\n\t have: \"requestedamis\"\n\t want (one of): [\"
instances\" \"launchconfigurations\" \"launchtemplates\" \"networkacls\" \"securitygroups\" \"subnets\" \"volumes\" \"v
pcs\"]"
2026-07-08T17:39:50Z INFO Detected config files num=1

Dockerfile (dockerfile)
=======================
Tests: 21 (SUCCESSES: 20, FAILURES: 1)
Failures: 1 (HIGH: 1, CRITICAL: 0)

AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument
тХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХРтХР
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

See https://avd.aquasec.com/misconfig/ds002
тФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФА


9 changes: 9 additions & 0 deletions submissions/trivy-fs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
2026-07-08T17:38:19Z INFO [vulndb] Need to update DB
2026-07-08T17:38:19Z INFO [vulndb] Downloading vulnerability DB...
2026-07-08T17:38:19Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2"
2026-07-08T17:38:28Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2"
2026-07-08T17:38:28Z INFO [vuln] Vulnerability scanning is enabled
2026-07-08T17:38:28Z INFO [secret] Secret scanning is enabled
2026-07-08T17:38:28Z INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2026-07-08T17:38:28Z INFO [secret] Please see also https://aquasecurity.github.io/trivy/v0.59/docs/scanner/secret#recommendation for faster secret detection
2026-07-08T17:38:28Z FATAL Fatal error fs scan error: scan error: scan failed: failed analysis: walk filesystem: walk dir error: unknown error with C:/Program Files/Git/src: lstat C:/Program Files/Git/src: no such file or directory
Loading