Commit graph

69 commits

Author SHA1 Message Date
8682cdce55 fix: resolve session salts at runtime so prod release boots
The @session_options module attribute used Application.compile_env, which
baked compile-time placeholders into the endpoint while runtime.exs set
the real values from SESSION_SIGNING_SALT / SESSION_ENCRYPTION_SALT env
vars. Phoenix detected the mismatch and refused to start (failing migrate
Job in k8s).

- Remove hardcoded salts from config/config.exs (no compile-time binding)
- Add stable per-env salts in dev.exs / test.exs so local + CI don't need
  the env vars
- Split static cookie opts (@static_session_options) from runtime-resolved
  opts in endpoint.ex; expose session_options/0 as an MFA tuple in socket
  connect_info so LiveView decodes sessions with the same runtime salts
- New ToweropsWeb.Plugs.RuntimeSession wraps Plug.Session, fetches salts
  from app env on first request, and caches the initialized opts in
  :persistent_term (zero per-request overhead after warm-up)
2026-05-12 16:26:54 -05:00
ea91dae0e6 fix: 6 medium-severity bugs (M2, M8, M9, M10, M11, M16)
- M2: get_user_by_email/1 uses lower(email) = lower(?) so User@Example.com
  and user@example.com resolve to the same account; closes a lookalike-
  registration / password-reset confusion risk.
- M8: webhook auth plug no longer echoes "Webhook authentication not
  configured" — the misconfiguration is logged server-side and the caller
  gets a generic Internal server error so endpoints can't be probed.
- M9: coverages controller logs the underlying KMZ build error and
  returns a generic "Failed to build KMZ" string — filesystem paths and
  zip internals no longer leak.
- M10: account-data controller no longer crashes with MatchError when an
  org has zero or multiple memberships; defaults to "member" and treats
  any owner membership as owner.
- M11: ActivityController switches to a hard whitelist
  (Atom.to_string/1 comparison) instead of String.to_existing_atom/1;
  unknown filter types are dropped silently and the atom table can't be
  grown from API input.
- M16: title-tracking MutationObserver is stored on `this` so destroyed()
  actually disconnects it — fixes a memory leak per LV navigation.
2026-05-12 11:38:13 -05:00
b252cb81b9 fix: 5 more high-severity bugs (H6, H8, H17, H18)
- H6: batch interface stats query in get_site_capacity_summary, replacing
  per-interface SELECT with a single grouped query
- H8: explicit expires_at check in MobileAuth and GraphQLAuth plugs as
  defense in depth — the query already filters expired rows, but a future
  refactor dropping the filter can't quietly re-enable revoked sessions
- H17: Reports LiveView (toggle/delete/run_now) now uses
  Reports.get_organization_report/2 to scope by organization_id, fixing
  IDOR where a user could manipulate other tenants' reports by ID
- H18: ToweropsWeb.Api.ParamFilter strips identity fields (id,
  organization_id, user_id, created_by_id, inserted_at, updated_at) from
  user-supplied API params; applied to devices, sites, checks, and
  escalation_policies create/update paths to prevent mass-assignment of
  tenant ownership fields if a changeset cast list ever drifts
2026-05-12 11:04:31 -05:00
ca7bb75472 fix: 11 critical security/correctness bugs from code audit
- C1: Replace innerHTML template literals with DOM textContent in sites_map.ts
- C2: Derive user_id from current_scope instead of client params in policy consent
- C3: Fix broken halt() in GDPR data export (orphaned _ = ... halt())
- C4: Add owner/admin authorization check to MembersController update/delete
- C5: Require HMAC signature on agent release webhook (was optional)
- C6: Fix Repo.all_by/2 (not a real Ecto function) → Repo.all with query
- C7: Move auth exemption to before_send callback in BruteForceProtection
- C8: Block </style>/<script injection in status page custom_css validation
- C9: Create secrets.example.yaml with placeholders; secrets.yaml already gitignored
- C10: Load Stripe key from STRIPE_SECRET_KEY env var instead of hardcoded value
- C13: Remove credentials from PubSub backup broadcast; channel resolves them

C11 (no force_ssl): by design — Cloudflared terminates TLS
C12 (device quota race): false positive — FOR UPDATE serializes correctly
2026-05-12 10:20:52 -05:00
39e588c686 fix: API token access control, admin form crash, MIB validation, CSP dedup
- Add organization membership check before API token creation
- Fix admin security allowlist form reading current_user instead of current_scope.user
- Use recursive File.ls instead of Path.wildcard to include hidden files in MIB validation
- Add upload size check before ZIP extraction in MIB controller
- Centralize CSP in SecurityHeaders plug with per-request nonces instead of 'unsafe-inline'
2026-05-12 09:08:57 -05:00
8338d0af1e fix: remaining bugs.md findings — Repo calls, process dict, CSS, rate limits, Oban, debug route
- Extract Repo calls from LiveViews into Agents.get_agent_token_for_org/2
  and Accounts.verify_new_totp_device/2 context functions
- Remove Process dictionary usage for cookie consent; use explicit assigns
- Reject url() references in status page custom CSS changeset
- Add per-organization rate limit check alongside per-IP
- Document Oban flush as emergency-only with audit logging
- Gate /admin/headers behind dev_routes (debug endpoint)
2026-05-11 19:34:18 -05:00
3632580d1b fix: address security and reliability findings from bugs.md review
- Client IP: only trust X-Forwarded-For from RFC 1918 proxy IPs
- Webhook auth: handle nil/blank secret with controlled error, not 500 crash
- Sudo redirect: reuse validated return_path? from login to prevent open redirect
- Map live: remove redundant inline script (ensureLeaflet hook handles loading)
- Bang calls: convert crash-prone exact matches to case in QR live and API controllers
2026-05-11 18:54:12 -05:00
833de33622 fix: vendor Leaflet 1.9.4 locally instead of fetching from unpkg
unpkg.com fetches were intermittently failing in production, leaving
the /sites-map page without a working map. Vendoring Leaflet (JS, CSS,
marker/layer images) under priv/static/vendor/leaflet/ removes the
runtime CDN dependency, the matching CSP relaxation, and the SRI hash
churn on future updates.

Add 'vendor' to ToweropsWeb.static_paths/0 so Plug.Static serves the
new tree, and revert script-src/style-src to no longer list unpkg.com.
connect-src still allows *.tile.openstreetmap.org for tile fetches.
2026-05-01 17:06:54 -05:00
a1e601563e fix: allow leaflet CDN and OSM tiles in CSP
The /sites-map page loads leaflet from unpkg.com (CSS + JS) and fetches
tiles from *.tile.openstreetmap.org. Update Content-Security-Policy to
permit those origins so the map can render.

Adds:
- script-src https://unpkg.com (leaflet.js)
- style-src https://unpkg.com (leaflet.css)
- connect-src https://*.tile.openstreetmap.org (tile fetches)
2026-05-01 17:03:14 -05:00
0d961e887f fix: allow Plausible analytics domain in CSP
Add https://a.w5isp.com to script-src and connect-src so the analytics
script loads and event POSTs are not blocked.
2026-04-29 14:24:04 -05:00
701ce12f08 perf+refactor: codebase-wide query and antipattern audit
Performance:
- schedule_live: preload page once instead of get_schedule!/1 per row
- alert_live + alerts: DB-side status filter; Repo.aggregate counts replace length/Enum.count over 500-row fetches on every event
- dashboard_live: drop duplicate get_device_status_counts; cap active alerts to 20 + use count_active_alerts/1
- maintenance.active_windows_for_device: 3 round-trips collapsed into one query (per-site-id branch keeps Postgres parameter types unambiguous)
- sites.build_site_tree: O(N^2) -> O(N) via group_by(parent_site_id)
- accounts.sole_owner_organizations: single group_by + having instead of per-org Repo.aggregate loop
- agents + agent_live (org + admin): count_assigned_devices_batch/1 + count_agent_polling_targets/1 (no preloads)
- alert_digest_worker: list_alerts_by_ids/1 batches digest fetch
- gaiia: distinct: true at DB; limit 50 on bidirectional ilike

Indexes:
- maintenance_windows(organization_id, starts_at, ends_at) WHERE suppress_alerts = true
- alerts(check_id) WHERE resolved_at IS NULL

Antipatterns:
- agents.delete_agent_token: PubSub.broadcast moved outside Repo.transaction so a rollback no longer leaves subscribers acting on a non-existent deletion
- integrations_controller.to_atom_keys: replaced String.to_existing_atom on user-controlled JSON keys with explicit allowlist
- 9 Task.start callsites converted to Task.Supervisor.start_child(Towerops.TaskSupervisor, ...) so background DB writes survive shutdown (test-mode discovery shims left as-is for sandbox semantics)
2026-04-28 16:58:51 -05:00
e822bc9986 chore: rate_limit rewrite + on-call redesign plan + handoff_notifications_mode 2026-04-28 12:17:29 -05:00
91e3181bbc dialyzer: fix all unmatched_return warnings (154 → 0)
Prefix fire-and-forget calls (PubSub.broadcast, Oban enqueue, Logger,
update_*, etc.) with `_ =` so dialyzer knows the return value is
intentionally discarded. Wrap a few if/case expressions whose
branches produce mixed types the same way.

No behavior changes — only explicit acknowledgement of discarded
returns.

Warnings: 242 → 88.
2026-04-21 10:03:55 -05:00
e64df28906 fix: correct login URL path in markdown negotiation content 2026-04-17 15:50:31 -05:00
ca041fe5aa feat: add Accept: text/markdown content negotiation for agent compatibility
Intercepts requests with Accept: text/markdown at endpoint level (before
the router's :accepts plug rejects them) and returns hardcoded markdown
representations for /, /privacy, and /terms.
2026-04-17 14:14:31 -05:00
f8b8082ee5 chore/update-deps (#236)
Reviewed-on: graham/towerops-web#236
2026-04-15 13:13:36 -05:00
c5481c2cd3 add mobile token auth to GraphQL endpoint (#48)
Extend the GraphQL API and socket to accept mobile session tokens
alongside existing API tokens. Add OrganizationScope middleware
that extracts organization_id from query args for mobile users.
Add my_organizations query for mobile app org listing.

Reviewed-on: graham/towerops-web#48
2026-03-16 15:23:07 -05:00
44fc017653
test: improve test coverage and code quality
- Update pre-commit config
- Add test coverage for URL validation, security modules, and monitoring executors
- Improve network map implementation and tests
- Add coverage for API controllers and LiveView endpoints
- Refactor code to fix Credo issues (reduce nesting, extract helper functions)
2026-03-15 14:49:55 -05:00
1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia)
- Jason.decode! → Jason.decode with error handling for untrusted input
- inspect() leak: replace with generic error messages, log details server-side
- SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes
- SSRF validation added to HTTP monitoring executor and integration credentials
- GraphQL complexity limits: always applied, not just in prod
- GraphQL introspection: also check GET query params, not just body
- Stripe webhook: explicit nil/empty checks for signature and body
- Cookie security: secure flag for session (prod), http_only+secure for remember_me
- Honeybadger API key: read from env var with fallback
- String.to_integer → Integer.parse with fallback for URL params
- String.to_atom → whitelist map for HTTP methods
- Gaiia webhook: remove secret_len and expected signature from log
- Admin API: add rate limiting pipeline
- to_atom_keys: per-key fallback instead of all-or-nothing rescue
2026-03-14 14:48:59 -05:00
532d88ffb9
perf: disable Req retry for faster error tests
- Add retry: false to VISP Client HTTP requests
- Add retry: false to ReleaseChecker GitHub API requests
- Remove unused Plug.Conn import from RemoteIpLogger
- Remove unused default parameter from req_get/2

Results:
- VISP sync test: 7007ms → 113ms (62x faster)
- ReleaseChecker test: 7004ms → 17ms (402x faster)
- UserResetPasswordLive test: 1495ms → 284ms (5x faster)

Req's default retry behavior (1s, 2s, 4s exponential backoff) was
causing 7-second delays for HTTP 500/503 error responses in tests.
For these clients, immediate failure is preferred over retries.
2026-03-10 16:19:31 -05:00
fc63a4faaf
fix: properly disable Plug.Telemetry logging for health checks
Set conn.private.log to false to prevent Plug.Telemetry from logging
health check requests in addition to phoenix_log and plug_skip_telemetry.
2026-03-08 16:39:26 -05:00
6d3fc15f4a
fix: allow data URLs in CSP for dynamic favicon
The dashboard status indicator favicon (colored circle) uses canvas
to generate a data URL that's set as the favicon. This was blocked
by CSP in staging/production because default-src didn't include data:.

Added data: to default-src directive to fix favicon not updating in
browser tabs when device status changes.
2026-03-06 13:51:31 -06:00
8f52d87854
feat: add rate limiting to admin endpoints (100 req/min)
- Add :admin type to RateLimit plug with 100 requests per minute per IP
- Apply rate limiting to all /admin routes (LiveView and controller actions)
- Protects superuser endpoints from abuse and brute force attempts
- Aligns with existing auth (10 req/min) and API (1000 req/min) limits
2026-03-05 14:11:19 -06:00
86a5dc728c
fix: comprehensive bug fixes from reliability audit
Critical Fixes (5):
- Fix Task.yield_many race condition causing data corruption in DevicePollerWorker
- Fix Enum.zip data corruption in SNMP Base Profile with length validation
- Fix missing Alert schema fields for check alerts (check_id, severity, etc.)
- Fix memory leaks from uncancelled LiveView timers in 4 components
- Fix PubSub subscription leak in device form credential testing

High Severity Fixes (3):
- Fix clock skew bug in needs_discovery? check with DateTime.diff clamping
- Fix nil crash in interface status display with proper nil handling
- Fix migration index names after equipment→devices table rename

Medium Severity Fixes (6):
- Fix race condition in device monitor worker (duplicate maintenance checks)
- Fix missing preload validation in devices.ex get_org_default_agent
- Fix broad rescue clause in alerts.ex with specific error handling
- Fix fire-and-forget notification tasks with try-catch error logging
- Fix LiveView state bleeding between tabs (assign_new → assign)
- Add catch-all handle_info callbacks to 3 LiveViews

Infrastructure:
- Silence health check endpoint logs (/health, /health/time)
- Add migration to fix equipment index names missed in rename

Files Changed: 16 files modified, 1 migration added
All changes compile successfully and are backward-compatible.
2026-03-05 08:53:30 -06:00
ec03a03cf5
fix: completely silence logs for noisy health check endpoints
Created FilterNoisyLogs plug that disables Phoenix logging for both
request and response logs by setting conn.private.phoenix_log to false.

This prevents both:
- Initial request log (Plug.Telemetry)
- Response log (Phoenix.Logger "Sent 200 in Xms")

Filters:
- GET /health (Kubernetes probes)
- HEAD / (external uptime monitors)
2026-03-05 08:23:12 -06:00
c10394808a
chore: remove unused functions from disabled brute force protection
Removed all unused private functions and imports from BruteForceProtection
plug since the feature is currently disabled (Cloudflare handles security).

Functions can be restored from git history if feature is re-enabled.

Fixed compilation warnings:
- unused function track_404_if_needed/2
- unused function maybe_track_404/2
- unused function get_remote_ip/1
- unused function get_fallback_ip/1
- unused function format_remote_ip/1
- unused function check_and_block/2
- unused function block_request/2
- unused function agent_websocket?/1
- unused alias BruteForce
- unused import Plug.Conn
2026-03-04 16:57:48 -06:00
8109bd71c8 fix: auto-dismiss poll gap insights when polling resumes
When a device resumes polling (last_snmp_poll_at updated), automatically
dismiss any active 'device_poll_gap' insights for that device.

This prevents stale insights from lingering after connectivity issues
are resolved (e.g., agent reconnection after being blocked).

Changes:
- Auto-dismiss device_poll_gap insights in update_snmp_poll_time()
- Log when insights are auto-dismissed for visibility
- Fix brute force protection compilation error
2026-03-04 16:19:28 -06:00
74173dad0b fix: temporarily disable brute force IP protection
Disable all IP blocking and 404 tracking to allow unrestricted access
while we review and tune ban thresholds and exemptions.

This is a temporary measure to prevent legitimate traffic from being
blocked. Will re-enable after proper configuration.
2026-03-04 14:05:39 -06:00
dce9253afc
fix: exempt agent WebSocket connections from brute force IP bans
Agent connections authenticate at the channel join level with token
validation, so they should not be subject to IP-based brute force
protection. This prevents legitimate agents from being banned when
connecting to the server.

Changes:
- Exempt /socket/agent/websocket from all IP ban checks
- Update documentation to reflect agent exemption
- Agents still authenticate securely via token during channel join
2026-03-04 13:53:13 -06:00
9ecd01dc8f Security hardening: remaining audit fixes
- Encrypt session cookies (add encryption_salt to endpoint)
- X-Frame-Options: SAMEORIGIN → DENY (match CSP frame-ancestors 'none')
- Remove unsafe-eval from CSP script-src
- Apply security headers in all environments (not just prod)
- Add GraphQL query complexity limit (max 500) via Absinthe.Plug
- GraphQL introspection already blocked in prod via plug

Closes audit items #6, #7, #12, #13, #14, #16
2026-02-15 12:54:38 -06:00
c790794191 Fix tests, credo issues, and Gaiia sync bugs
- Fix CLOAK_KEY placeholder in nix shell (invalid base64)
- Fix error HTML test assertion to match actual template
- Fix Gaiia sync: read 'subnet' field instead of 'block' for IP blocks
- Fix Gaiia sync: extract nested status name from subscription objects
- Fix security headers tests for environment detection
- Fix mobile auth tests to use raw_token
- Fix LiveView test assertions to match actual template content
- Fix SNMP config test expectations for test environment
- Refactor: resolve all Credo complexity/nesting issues
  - Extract helper functions in NetBox sync, VISP sync, Sonar sync
  - Flatten nested conditionals in settings, integrations, alerts
  - Use with/validate_present pattern for connection testing
2026-02-15 11:55:49 -06:00
34fe5d7e49 Security fixes: mask credentials in logs/API, fix cookie/CSP/LIKE injection/webhooks
CRITICAL fixes:
- Mask SNMP community string in agent channel logs (CRITICAL-1)
- Remove snmpv3 passwords from REST API responses, return _set booleans (CRITICAL-2)
- Replace snmp_community with snmp_community_set in GraphQL type (CRITICAL-3)

HIGH fixes:
- Fix cookie same_site from invalid 'Towerops' to 'Lax' (HIGH-4)
- Remove unsafe-eval from CSP script-src (HIGH-6)
- Block GraphQL introspection queries in production (HIGH-7)
- Sanitize LIKE wildcards in SNMP device name search (HIGH-8)
- Reject webhooks when no secret configured instead of accepting (HIGH-9)

MEDIUM fixes:
- Hash mobile session tokens (SHA-256) before DB storage (MEDIUM-10)
- Apply security headers in all environments, not just prod (MEDIUM-14)
- Add GraphQL query complexity limit (500) in production (MEDIUM-16)
- Fix X-Frame-Options to DENY to match frame-ancestors 'none' (MEDIUM-13)
2026-02-15 09:09:04 -06:00
8752dfec49 fix: netbox url field type, gaiia ipRange→block, remove unknown webhook log 2026-02-14 17:44:01 -06:00
a3730db579 i18n: add Spanish locale with full translations, language selector, and locale hook 2026-02-14 17:44:01 -06:00
243b773e39 i18n: wrap all user-facing strings in gettext() 2026-02-14 17:44:01 -06:00
afa58df888
fix: add Permissions-Policy header to suppress browsing-topics warning
Added Permissions-Policy header with browsing-topics=() to suppress console
warnings about unrecognized Privacy Sandbox API features. Header is now applied
in all environments (dev and prod) while other security headers remain
production-only.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 13:31:08 -06:00
8d9d385683
fix: prevent AlreadySentError in BruteForceProtection plug
The plug was attempting to register a before_send callback after
blocking a banned IP and sending a 403 response. This caused
Plug.Conn.AlreadySentError in production.

Fixed by checking conn.halted before attempting to register the
404 tracking callback. If the request was already blocked and sent,
we skip the callback registration.

Also updated the test to verify proper behavior without the
workaround for AlreadySentError.
2026-02-12 12:53:32 -06:00
4d73e77f3a
Add 404-based brute force protection system
Implements automated detection and blocking of IPs exhibiting scanning behavior (5+ unique 404s within 1 minute).

Key features:
- Progressive ban escalation (5 min → 1 hour → permanent)
- CIDR range and exact IP whitelisting
- Redis-backed 404 path tracking with 60s TTL
- Cloudflare WAF integration for permanent bans
- Admin UI for whitelist and blocked IP management
- Oban cron jobs for cleanup (expired bans, stale violations)
- ETS caching for whitelist performance

Components:
- Plug: ToweropsWeb.Plugs.BruteForceProtection
- Context: Towerops.Security.BruteForce
- Schemas: IpBlock, IpWhitelist
- Workers: CloudflareBanWorker, ExpiredBanCleanupWorker, StaleViolationCleanupWorker
- Admin UI: /admin/security (superuser only)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 16:31:48 -06:00
c7df6a8569
Add CI-triggered mass agent update webhook
POST /api/v1/webhooks/agent-release triggers all connected agents
to self-update via their existing WebSocket channels. Authenticated
with a shared secret (AGENT_WEBHOOK_SECRET env var).

- Add ReleaseChecker.invalidate_cache/0 for fresh GitHub fetch
- Add Agents.list_updatable_agents/0 (enabled + seen < 10min)
- Add Agents.broadcast_mass_update/0 orchestration function
- Add WebhookAuth plug with timing-safe secret comparison
- Add AgentReleaseWebhookController with :webhook pipeline
- Configure AGENT_WEBHOOK_SECRET in dev/test/runtime configs
2026-02-10 13:40:32 -06:00
631d03536e
security: add session inactivity timeout and comprehensive security documentation
Session Security Enhancements:
- Add 30-minute inactivity timeout to UpdateSessionActivity plug
- Automatic logout with user notification after inactivity
- Session timeout check happens on every request
- Graceful logout with flash message
- Refactored for Credo compliance (reduced nesting depth)

Security Documentation:
- Create comprehensive SECURITY.md covering all security features
- Document authentication, authorization, and encryption
- List OWASP Top 10 mitigations
- Include security checklist for developers
- Document recent security fixes and improvements

Verification:
- Password reset already rate-limited (confirmed)
- All 6,145 tests passing
- Zero Credo issues
- Zero security warnings

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 10:47:44 -06:00
d431931cfe
update hammer 2026-02-08 10:08:31 -06:00
c93144cc37
snmp v3 support 2026-02-04 12:02:38 -06:00
02833d0b44 Add rate limiting to auth and API endpoints using Hammer 2026-02-01 14:59:54 -06:00
7ca07010ab
Use detected timezone from session in user registration 2026-02-01 12:18:33 -06:00
77e62df9da
Move CaptureTimezone plug to browser pipeline
The plug was incorrectly placed in the endpoint, causing it to run on
all requests including APIs. Moved to :browser pipeline where it belongs,
and removed redundant fetch_session call since the pipeline handles it.
2026-02-01 11:15:51 -06:00
d05c493943
Add CaptureTimezone plug to browser pipeline 2026-02-01 11:10:34 -06:00
c4e8c70e7d
Add CaptureTimezone plug to extract cf-timezone header 2026-02-01 11:02:57 -06:00
25397139c1 nif ci fix complie in ci 2026-01-30 13:01:44 -06:00
de1ad8bc8a nif ci fixes and dialyzer fixes 2026-01-30 12:38:50 -06:00
55e9397d59 user session tracking 2026-01-29 14:14:21 -06:00