Skip to content
Merged

Dev #314

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c3f7862
chore(deps): bump node from 26.8-alpine to 26.10-alpine in /src/frontend
dependabot[bot] Oct 1, 2026
61a6e6e
chore(deps)(deps): bump numpy from 2.5.2 to 2.5.3 in /src/backend
dependabot[bot] Oct 1, 2026
b802ef8
chore(deps)(deps): bump pymysql from 1.2.0 to 1.2.3 in /src/backend
dependabot[bot] Oct 1, 2026
c099ef9
chore(deps)(deps): bump pypdf from 6.16.2 to 6.19.0 in /src/backend
dependabot[bot] Oct 1, 2026
2b2dfc9
chore(deps)(deps): update faker requirement in /src/backend
dependabot[bot] Oct 1, 2026
19260f1
chore(deps)(deps): bump @primeuix/themes in /src/frontend/app
dependabot[bot] Oct 1, 2026
5fa204d
chore(deps)(deps): bump uvicorn from 0.52.4 to 0.54.0 in /src/backend
dependabot[bot] Oct 1, 2026
0069369
chore(deps)(deps): update python-gvm requirement in /src/backend
dependabot[bot] Oct 1, 2026
37fed06
chore(deps)(deps-dev): bump sass in /src/frontend/app
dependabot[bot] Oct 1, 2026
73186b9
patch CSRF Token on production mode
maldwg Oct 2, 2026
d74a303
Merge pull request #310 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
9e35194
chore(deps)(deps): bump vue from 3.5.42 to 3.5.43 in /src/frontend/app
dependabot[bot] Oct 2, 2026
39368f7
chore(deps)(deps-dev): bump @vue/test-utils in /src/frontend/app
dependabot[bot] Oct 2, 2026
7e6c1ef
chore(deps)(deps-dev): bump vitest in /src/frontend/app
dependabot[bot] Oct 2, 2026
e64c5b1
chore(deps)(deps): bump vuetify in /src/frontend/app
dependabot[bot] Oct 2, 2026
46a61a8
update dependabot
maldwg Oct 2, 2026
e64b868
Merge pull request #309 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
e42d2b2
Merge pull request #308 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
bb9d2b3
Merge pull request #307 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
9e04fde
Merge pull request #306 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
53a3de2
Merge pull request #302 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
5842033
Merge pull request #294 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
615f858
Merge pull request #299 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
4798586
Merge pull request #295 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
c74c1a8
Merge pull request #293 from bwInfoSec/dependabot/docker/src/frontend…
maldwg Oct 2, 2026
0f1d6d9
chore(deps)(deps-dev): bump @vitejs/plugin-vue in /src/frontend/app
dependabot[bot] Oct 2, 2026
660ab59
Merge pull request #303 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
6ebf957
Merge pull request #298 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
0d1820a
factor in sucessful PRs for dependabot
maldwg Oct 2, 2026
8642974
chore(deps)(deps): bump pandas from 3.0.5 to 3.0.6 in /src/backend
dependabot[bot] Oct 2, 2026
42566e7
chore(deps)(deps): bump sqlalchemy from 2.0.52 to 2.1.1 in /src/backend
dependabot[bot] Oct 2, 2026
a0221bc
chore(deps)(deps-dev): bump @playwright/test in /src/frontend/app
dependabot[bot] Oct 2, 2026
642df47
chore(deps)(deps-dev): bump jsdom in /src/frontend/app
dependabot[bot] Oct 2, 2026
c40c5a2
Merge pull request #305 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
861e161
Merge pull request #296 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
9b26628
Merge pull request #297 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
613d36a
Merge pull request #301 from bwInfoSec/dependabot/npm_and_yarn/src/fr…
maldwg Oct 2, 2026
3b16425
fix e2e tests
maldwg Oct 2, 2026
7089a56
Merge pull request #304 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
385c3b1
Merge pull request #311 from bwInfoSec/fix/csrf-production-issues
maldwg Oct 2, 2026
434a947
Merge branch 'main' into dev
maldwg Oct 2, 2026
6aff53b
Merge pull request #300 from bwInfoSec/dependabot/pip/src/backend/dev…
maldwg Oct 2, 2026
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
7 changes: 0 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ updates:
commit-message:
prefix: "chore(deps)"
include: "scope"
ignore:
# Ignore major version updates for Vue and Vuetify to avoid breaking changes
- dependency-name: "vue"
update-types: ["version-update:semver-major"]
- dependency-name: "vuetify"
update-types: ["version-update:semver-major"]

# Docker base images
- package-ecosystem: "docker"
directory: "/src/backend"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
run: >-
npm test -- --run
src/services/__tests__/passwordPolicy.spec.ts
src/services/__tests__/csrf.spec.ts
src/stores/__tests__/auth.spec.ts
src/views/__tests__/Login.spec.ts

Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Dependabot Auto-Merge

on:
pull_request_target:
branches: [dev]
types: [opened, reopened, synchronize, ready_for_review, edited]

permissions: {}

concurrency:
group: dependabot-auto-merge-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
auto-merge:
name: Auto-merge minor and patch updates into dev
if: >-
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.base.ref == 'dev' &&
!github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 65
permissions:
actions: read
contents: write
pull-requests: write
steps:
# This privileged workflow never checks out or executes PR code.
# Default metadata verification requires Dependabot-authored commits.
- name: Fetch verified Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

# update-type is the highest version change in the PR, including groups.
# Major and unclassified updates remain available for manual review.
- name: Wait for successful PR pipelines on the exact head commit
if: >-
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail
deadline=$((SECONDS + 3600))

while (( SECONDS < deadline )); do
# Select the latest run of each required PR workflow. Push runs,
# earlier commits, and this automation's own run cannot satisfy it.
pipelines="$(gh api --paginate --slurp \
"repos/$REPOSITORY/actions/runs?event=pull_request&head_sha=$PR_HEAD_SHA&per_page=100" \
| jq --arg sha "$PR_HEAD_SHA" '
[ .[].workflow_runs[]
| select(.event == "pull_request" and .head_sha == $sha)
| select(.path == ".github/workflows/ci.yml" or
.path == ".github/workflows/e2e.yml") ]
| group_by(.path)
| map(max_by(.id))
')"

if jq -e 'any(.[]; .status == "completed" and .conclusion != "success")' \
<<< "$pipelines" >/dev/null; then
echo "A PR pipeline failed, was cancelled, or was skipped; refusing auto-merge."
exit 1
fi

if jq -e 'length == 2 and all(.[]; .status == "completed" and .conclusion == "success")' \
<<< "$pipelines" >/dev/null; then
echo "CI and E2E passed for $PR_HEAD_SHA."
exit 0
fi

echo "Waiting for both PR pipelines to succeed for $PR_HEAD_SHA."
sleep 20
done

echo "Timed out waiting for PR pipelines; refusing auto-merge."
exit 1

- name: Approve and enable auto-merge for minor and patch updates
if: >-
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash --match-head-commit "$PR_HEAD_SHA" "$PR_URL"
7 changes: 7 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ jobs:
E2E_FRONTEND_SERVER: preview
run: ./scripts/e2e/run-local.sh

- name: Run production CSRF auth smoke tests
env:
E2E_SKIP_CLEANUP: '1'
E2E_FRONTEND_SERVER: preview
E2E_ENV: production
run: ./scripts/e2e/run-local.sh --grep 'bootstraps the initial admin account|authenticates, updates the profile, and rotates the password' --output=test-results/production --reporter=line

- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v7
Expand Down
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ release notes whenever they change operator-visible behavior.
promotions or urgent hotfixes.
- Keep each pull request focused on one logical change.

Dependabot targets `dev` for dependency updates. Verified minor and patch
updates are automatically approved and set to auto-merge using squash merges
only after both CI Pipeline and E2E Tests succeed for the PR's exact head
commit. Missing, pending, failed, cancelled, or skipped pipelines cannot
authorize a merge. Branch protection rules still apply. Major updates
(including Vue and Vuetify) and updates without a recognized version change
remain open for manual review.

For this automation, enable **Allow auto-merge** and squash merging in the
repository settings, and **Allow GitHub Actions to create and approve pull
requests** under Actions → General → Workflow permissions. The explicit
pipeline gate applies even without required-check rules on `dev`; configuring
those rules also protects manual merges. The workflow does not bypass branch
protection or organization policies.

Create a branch from the latest `dev` branch:

```bash
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ Run it locally with:
npm --prefix src/frontend/app run test:e2e:local
```

The fresh-stack runner explicitly uses `ENV=development`, independent of your local `.env`. To verify bootstrap, profile updates, and password rotation with production CSRF protection enabled:

```bash
E2E_ENV=production bash scripts/e2e/run-local.sh --grep 'bootstraps the initial admin account|authenticates, updates the profile, and rotates the password'
```

To watch the browser while the suite runs, use:

```bash
Expand Down
5 changes: 3 additions & 2 deletions deploy/release/docker-compose.release.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ services:
backend:
image: ghcr.io/bwinfosec/vulnerabilityhub-backend:__VERSION__@__BACKEND_DIGEST__
ports:
- "${BACKEND_PUBLISHED_PORT:-8000}:8000"
# The browser reaches the API through the frontend's /backend proxy.
# Keep the diagnostic host port local so internet probes cannot bypass it.
- "127.0.0.1:${BACKEND_PUBLISHED_PORT:-8000}:8000"
volumes:
- report_storage:/app/reports
- upload_storage:/app/upload
Expand Down Expand Up @@ -151,4 +153,3 @@ networks:
driver: bridge
backend-network:
driver: bridge

1 change: 1 addition & 0 deletions deploy/release/production.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Public endpoints
FRONTEND_PUBLISHED_PORT=80
# The release Compose file binds this diagnostic port to 127.0.0.1 only.
BACKEND_PUBLISHED_PORT=8000
FRONTEND_URL=https://vulnerabilityhub.example.com
CORS_ORIGINS=https://vulnerabilityhub.example.com
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ services:
backend:
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
environment:
# Keep the dev smoke suite independent of a local release .env.
# Set E2E_ENV=production to exercise production CSRF protection.
- ENV=${E2E_ENV:-development}
- CORS_ORIGINS=http://127.0.0.1:${E2E_FRONTEND_PORT:-4173},http://localhost:${E2E_FRONTEND_PORT:-4173}
- FRONTEND_URL=http://127.0.0.1:${E2E_FRONTEND_PORT:-4173}
- SMTP_SERVER=mailhog
Expand Down
2 changes: 1 addition & 1 deletion scripts/e2e/run-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ npm --prefix src/frontend/app exec playwright install chromium
echo "[e2e] Resetting e2e stack"
compose --profile dev down -v --remove-orphans

echo "[e2e] Starting backend services in dev mode"
echo "[e2e] Starting backend services in ${E2E_ENV:-development} mode"
compose --profile dev up -d --build db clamav ldap backend mailhog

"$ROOT_DIR/scripts/e2e/wait-for-url.sh" "http://127.0.0.1:${E2E_BACKEND_PORT}/health" 180
Expand Down
30 changes: 30 additions & 0 deletions scripts/release/smoke-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,34 @@ fi
curl --fail --silent --show-error http://127.0.0.1:18000/health >/dev/null
curl --fail --silent --show-error http://127.0.0.1:18080/ >/dev/null

# Exercise the production-only CSRF middleware through the same /backend proxy
# used by the browser. A valid token must reach routing (404 for this synthetic
# path), while an absent token must be a handled 403 rather than an ASGI 500.
csrf_headers="$smoke_dir/csrf-headers.txt"
csrf_cookies="$smoke_dir/csrf-cookies.txt"
curl --fail --silent --show-error \
--dump-header "$csrf_headers" \
--cookie-jar "$csrf_cookies" \
http://127.0.0.1:18080/backend/users/has-admin >/dev/null
csrf_token="$(awk 'BEGIN { IGNORECASE=1 } /^X-CSRF-Token:/ { gsub("\\r", "", $2); print $2 }' "$csrf_headers")"

if [ -z "$csrf_token" ]; then
echo "Release backend did not issue a CSRF token." >&2
exit 1
fi

protected_status="$(curl --silent --output /dev/null --write-out '%{http_code}' \
--request POST \
--cookie "$csrf_cookies" \
--header "X-CSRF-Token: $csrf_token" \
http://127.0.0.1:18080/backend/__csrf-smoke)"
missing_status="$(curl --silent --output /dev/null --write-out '%{http_code}' \
--request POST \
http://127.0.0.1:18080/backend/__csrf-smoke)"

if [ "$protected_status" != "404" ] || [ "$missing_status" != "403" ]; then
echo "Release CSRF smoke test failed (valid=$protected_status, missing=$missing_status)." >&2
exit 1
fi

echo "Release image smoke test passed."
6 changes: 5 additions & 1 deletion src/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@
app.add_middleware(
CSRFProtectionMiddleware,
secret_key=settings.SECRET_KEY,
secure_cookie=True, # Use secure cookies in production
# Release deployments may run directly over HTTP or terminate TLS at
# the public frontend. Match the cookie flag to the configured public
# URL so browsers do not silently discard it on HTTP installations.
secure_cookie=settings.FRONTEND_URL.lower().startswith("https://"),
)

# Rate Limiting Middleware
Expand Down Expand Up @@ -115,6 +118,7 @@ async def target_managed_network_exception_handler(
allow_headers=["Content-Type", "Authorization", "X-CSRF-Token"], # Explicit headers
expose_headers=[
"Content-Disposition",
"X-CSRF-Token",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-RateLimit-Reset",
Expand Down
35 changes: 22 additions & 13 deletions src/backend/middleware/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
HTTP requests (POST, PUT, PATCH, DELETE).

The middleware generates a cryptographically secure token and stores it in an
HttpOnly cookie. Clients must include this token in the X-CSRF-Token header
for protected requests.
HttpOnly cookie. Safe responses expose the same token in the X-CSRF-Token
header so clients can echo it on protected requests.
"""

import logging
import secrets
from typing import Callable, Set

from fastapi import Request, HTTPException, status
from fastapi import Request, status
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from starlette.responses import JSONResponse, Response


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -104,8 +104,6 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:
Returns:
Response, potentially with a new CSRF token cookie

Raises:
HTTPException: If CSRF validation fails (403 Forbidden)
"""
# Skip CSRF protection for exempt paths
if self._is_path_exempt(request.url.path):
Expand All @@ -128,7 +126,11 @@ def _is_path_exempt(self, path: str) -> bool:
Returns:
True if the path is exempt, False otherwise
"""
return any(path.startswith(exempt_path) for exempt_path in self.exempt_paths)
return any(
path == exempt_path
or path.startswith(f"{exempt_path.rstrip('/')}/")
for exempt_path in self.exempt_paths
)

async def _handle_safe_method(
self,
Expand Down Expand Up @@ -181,8 +183,6 @@ async def _handle_protected_method(
Returns:
Response if validation succeeds

Raises:
HTTPException: If CSRF token is missing or invalid (403 Forbidden)
"""
csrf_cookie = request.cookies.get(self.CSRF_COOKIE_NAME)
csrf_header = request.headers.get(self.CSRF_HEADER_NAME)
Expand All @@ -195,9 +195,14 @@ async def _handle_protected_method(
request.url.path,
request.client.host if request.client else "unknown"
)
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
detail="CSRF token missing. Include X-CSRF-Token header with your request."
content={
"detail": (
"CSRF token missing. Include X-CSRF-Token header "
"with your request."
)
},
)

# Validate token using constant-time comparison
Expand All @@ -208,9 +213,13 @@ async def _handle_protected_method(
request.url.path,
request.client.host if request.client else "unknown"
)
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
detail="CSRF token invalid. The token has expired or is incorrect."
content={
"detail": (
"CSRF token invalid. The token has expired or is incorrect."
)
},
)

logger.debug("CSRF token validated successfully for %s", request.url.path)
Expand Down
2 changes: 1 addition & 1 deletion src/backend/requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ pytest>=9.1.1
pytest-cov>=7.1.0
pytest-asyncio>=1.4.0
httpx>=0.28.1
faker>=40.37.0
faker>=40.39.0
12 changes: 6 additions & 6 deletions src/backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
## Last updated: 2025-11-26
# Core Framework
fastapi==0.141.1
uvicorn[standard]==0.52.4
uvicorn[standard]==0.54.0
pydantic==2.13.5
pydantic-settings==2.15.0
pydantic[email]==2.13.5

# Database
SQLAlchemy==2.0.52
PyMySQL==1.2.0
SQLAlchemy==2.1.1
PyMySQL==1.2.3
# Security & Authentication
bcrypt==5.0.0
python-jose[cryptography]==3.5.0
Expand All @@ -31,14 +31,14 @@ python-dotenv==1.2.3
ldap3==2.9.1

# Greenbone Management Protocol client
python-gvm>=27.7.0,<28.0
python-gvm>=27.10.0,<28.0

# Antivirus
clamd==1.0.2

# Data Processing
pandas==3.0.5
numpy==2.5.2
pandas==3.0.6
numpy==2.5.3

# Rate Limiting
slowapi==0.1.10
Expand Down
Loading
Loading