Login is now by email, not username. username still exists internally
(JWT sub claim, audit logs, Jira actor attribution, SSO provisioning all
still key off it) but is now always kept equal to email everywhere a user
is created or their email changes — never a separately-chosen value.
- User.email is now unique + NOT NULL (migration b067 backfills any
missing/blank email from username first, so existing rows — notably
the seeded admin, which historically had none — never violate it).
- /auth/login and the (unused but updated for consistency)
authenticate_user() now query by email.
- create_user (legacy, unreferenced but kept) and
create_user_without_password both derive username from email.
- update_user keeps username in sync when email changes, and rejects
duplicate emails.
- seed.py reads ADMIN_EMAIL (new env var, wired through install.sh and
docker-compose.prod.yml) for the initial admin; falls back to an
email-shaped ADMIN_USERNAME or a placeholder that's flagged for the
operator to fix.
- admin_config.py's import bundle now matches/creates users by email,
skipping (not crashing on) entries with no email.
- sso_service.py always sets username = email for SSO-provisioned users.
- LoginPage/auth.ts updated to email input/copy (wire field name stays
'username' — that's the OAuth2PasswordRequestForm spec, not the value).
- Renamed the password-setup webhook (email_webhook.*, /system/email-webhook-config)
and made it the single transport for all notification emails.
- New webhook_email_service.py shared by password_setup_service and
notification_service.
- Wired test validated/rejected, campaign completed, and new-MITRE-technique
notifications through the webhook (previously dead SMTP code, never triggered).
- Added POST /system/email-webhook-test to send a real test email.
- Hid the Email/SMTP settings tab from the UI (SMTP code kept intact, unused).
- Redact email_webhook.api_key on config export.
_REDACTED_KEYS listed jira.api_token/jira.password/tempo.api_token, but
routers/system.py actually writes admin credentials under
jira.admin_api_token and tempo.admin_token. The mismatch meant every
config export bundle included the live Jira and Tempo admin API tokens
in plaintext instead of redacting them. Found while researching the
Jira/Tempo integration for a service-account request; added a
regression test.
- config.py: move REPORT_OUTPUT_DIR from /tmp (world-writable) to /app/reports
to prevent CWE-377 symlink attack vector (B108, only real security issue)
- main.py: log startup seed failures instead of silently swallowing them (B110)
- Add # nosec annotations to intentional try/except patterns that are by design:
Jira integration errors, email failures, DetachedInstanceError, storage errors,
and Jira session timeout (all B110/B112 false positives)
- Add # nosec B105 to false positives where bandit misidentifies config key
names and masking strings as hardcoded passwords
- Add .bandit config to skip B311 in seed_demo.py (random used for fake
demo data generation, not cryptographic purposes)