Commit graph

24 commits

Author SHA1 Message Date
7c1aea50ec fix: M19, L16 — protobuf validation on 6 decode paths, vault ETS docs
- M19: Add validate_mikrotik_device, validate_mikrotik_command, validate_check,
  validate_snmp_query, validate_sensor, and validate_interface functions with
  string length and list size validation, wired into all call sites
- L16: Document that Cloak Vault ETS table name collision risk is mitigated by
  ETS tables being node-local (only a concern on same-BEAM-node, not a supported
  production configuration)
2026-05-12 15:46:13 -05:00
f87657fbfb fix: M20, M21, M22, L14 — archive safety, ETS protection, impersonation expiry, check pagination
- M20: Add check_no_hardlinks and check_no_special_files to MIB upload
  archive extraction to prevent hard link, device node, and FIFO attacks
- M21: Change RateLimit ETS table from :public to :protected; route hit/get/reset
  through GenServer.call instead of direct ETS access
- M22: Add 8-hour impersonation timeout — store impersonated_at in session and
  auto-revoke impersonation when expired
- L14: Add default limit (500) and optional offset to list_checks for pagination
2026-05-12 14:22:20 -05:00
a9579b84f0 fix: H11 — load session salts from env vars instead of hardcoded 8-byte values
SESSION_SIGNING_SALT, SESSION_ENCRYPTION_SALT, and LIVE_VIEW_SIGNING_SALT
are now loaded from environment variables in production. Dev/test keep the
previous defaults via config.exs. k8s/secrets.yaml has placeholder entries;
the user fills in real values before applying.
2026-05-12 14:03:31 -05:00
095c5d3236 fix: M1 — prevent cross-org resource existence probing via scoped fetch
Use Repo.get_by(schema, id: id, organization_id: organization_id) instead of
Repo.get + pattern match so that resources from wrong orgs return :not_found
instead of :forbidden, preventing org membership discovery.
2026-05-12 13:44:19 -05:00
c473f472c7 fix: L3, L7, L12, L13 — IP whitelist normalization, org scoping, liveSocket guard, changeset safety
- L3: Normalize IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) to IPv4 tuples
  when matching against IP whitelist entries
- L7: Verify interface belongs to current org before set/clear capacity
- L12: Guard window.liveSocket behind NODE_ENV check for production safety
- L13: Replace Alert.changeset with Ecto.Changeset.change for simple field
  updates (resolved_at, acknowledged_at, gaiia_impact) to prevent
  accidental overwrites from the 17-field cast
2026-05-12 13:18:06 -05:00
cec8eabcd8 fix: M13, M14, M15, M18 — download safety, queue blocking, PubSub chunking, protobuf validation
- M18: Add validate_snmp_device/1 with 255-byte limits on community,
  v3_auth_password, and v3_priv_password to prevent memory exhaustion
- M13: Add client-side mime_type allowlist to phx:download handler
- M14: Replace Process.sleep loop in WeatherSyncWorker with Task.async_stream
  to avoid blocking the Oban queue slot
- M15: Chunk device list in CloudLatencyProbeWorker PubSub broadcasts
  (100 devices per message) to prevent megabyte-sized messages
2026-05-12 12:54:48 -05:00
6fa0b791f2 fix: M3, M6, M7, M12 — open redirect, overfetch, missing constraint, admin defense-in-depth
- M3: Replace blacklist-based valid_return_path? with whitelist of known app
  route prefixes, plus URI decoding to prevent %2f encoding bypasses
- M6: Replace full-org device/link load in get_node_detail with targeted
  join query filtering by discovered node fields
- M7: Add partial unique index on (organization_id, email) for pending
  invitations (WHERE accepted_at IS NULL)
- M12: Add on_mount superuser verification to all 7 admin LiveViews for
  defense-in-depth
2026-05-12 12:46:16 -05:00
d456283844 fix: H22 — batch data retention deletes in 10k-row chunks
Replace 8 unbounded DELETE statements with a batched_delete loop to prevent
WAL amplification, autovacuum storms, and long row locks on large tables.
2026-05-12 12:26:50 -05:00
52cde2cbc5 fix: H21 — stagger per-integration sync jobs across maintenance queue
Sonar, Splynx, VISP, NetBox, and Gaiia sync workers all followed the same
"cron runs, then Enum.map syncs every integration in the same job" shape.
A single slow tenant — anything from an upstream rate limit to a stuck
GraphQL call — would hold the maintenance queue slot for ten minutes
while every other integration waited in line.

Each worker now uses the dispatcher/per-integration pattern that
UispSyncWorker already had:

- the cron `perform(%Oban.Job{args: %{}})` fans out one job per eligible
  integration via `PollingOffset.calculate_offset/2`, staggered across a
  5- to 10-minute window with `unique: [period: window_seconds]`
- `perform(%Oban.Job{args: %{"integration_id" => id}})` does the actual
  sync, classifying failures via `Towerops.Workers.SyncErrors.transient?/1`
  so permanent errors (auth, 4xx) don't get retried forever

Worker-specific defaults preserved:
- gaiia / netbox: 30/600s window, longer interval
- sonar / splynx / visp: 10/300s window

Tests updated to match the two-stage dispatcher pattern — `perform_job/2`
with empty args now asserts the per-integration jobs got enqueued and
the actual sync is invoked via a separate `perform_job/2` call with the
integration_id arg.
2026-05-12 12:12:11 -05:00
8eb8334459 remove already-fixed H25 from bugs.md 2026-05-12 11:47:16 -05:00
8379664cb0 fix: 3 more bugs (M17, M23, L9)
- M17: default SNMPv3 auth protocol on the four SNMP executors
  (interface/storage/processor/sensor) is now SHA-256 instead of MD5.
  Devices that explicitly set MD5 still use it; only the fallback for
  unconfigured devices changes.
- M23: permissions check now logs a warning when an org's memberships
  aren't preloaded so a silent denial is visible at the call site
  instead of looking identical to a real authz failure.
- L9: extracted the duplicated should_sync?/1 logic from gaiia, sonar,
  splynx, visp, and netbox sync workers into
  Towerops.Integrations.due_for_sync?/2. Each worker now passes its
  own default interval (10 or 30 minutes) to a single shared helper.
  Tests updated to cover the helper directly.
2026-05-12 11:45:22 -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
abfd44fd75 fix: 3 more high-severity bugs (H19, H20, H25)
- H19: /api/v1/mobile/auth/qr/verify no longer returns user_email. Knowing
  a QR token now only tells the caller the token is valid; the email is
  only revealed by /complete which consumes the token.
- H20: CoverageWorker max_attempts dropped from 3 to 1. The fail/2 path
  already returns :ok and writes the failure onto the coverage record,
  so the 3-attempt retry policy was never reachable and was misleading.
- H25: ChecksController.create rejects device_ids that don't belong to
  the caller's organization (via ScopedResource.fetch). Without this an
  API token could attach service checks to devices in another tenant.

Also strip bug ID references from in-code comments (per feedback that
H/M/L numbers don't survive past their bugs.md removal); commit history
keeps the audit trail.
2026-05-12 11:32:20 -05:00
d9afb6a3e4 remove fixed bugs (H12, L2, L5, L6, L8, L10, L11) from bugs.md 2026-05-12 11:27:32 -05:00
6a9b471d1f fix: 4 more high-severity bugs (H23, H26, H28, H29)
- H23: introduce Towerops.Workers.SyncErrors.transient?/1 classifier;
  uisp/preseem/cn_maestro sync workers now retry transient failures (5xx,
  timeouts, rate limits) via Oban while still discarding permanent errors
  (401/403/404) as :ok. Stale monitoring data is the bigger risk than an
  extra retry on a known-bad credential.
- H26: add Monitoring.get_latency_data_for_devices/2 batch counterpart
  (currently a stub map but called from SiteLive.Show so any future ping
  implementation lands in a single query, not one-per-device).
- H28: Nominatim search popups in the coverage map now bind via a text
  node instead of an HTML string, so a compromised/poisoned response
  can't execute as JS in the popup.
- H29: yaml_profiles dot-boundary OID prefix match (handles optional
  trailing dot from YAML profile keys). Stops 1.3.6.1.4.1.9 from
  incorrectly matching 1.3.6.1.4.1.99.
2026-05-12 11:14:03 -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
7e6ec7098c fix: 10 high-severity bugs from code audit (H1-H5, H14-H16, H27, H30)
- H1: batch count queries in MobileController (org sites/devices/alerts, site
  device counts) — reduces 1+3N+2M queries to 4 + 2
- H2: batch Oban cancel for stop_device_checks — single ANY(?) query instead
  of one per check
- H3: resolve_active_alerts_for_device uses update_all + deduped PubSub
  broadcasts (was N updates + N broadcasts)
- H4: atomic ON CONFLICT upsert for auto-discovery checks; restore partial
  unique index dropped in 20260404000002 (dedup migration included)
- H5: wrap apply_agent_to_all_equipment delete+insert in transaction so a
  failed insert_all rolls back the prior delete
- H14: replace String.to_integer/1 on untrusted SNMP OID components with
  Integer.parse/1 + validation (neighbor_discovery, discovery storage index,
  printer supply index)
- H15: ActivityFeedLive whitelists activity types against @all_types instead
  of String.to_existing_atom/1
- H16: defensive page param parsing in user_settings and admin dashboard
  LiveViews
- H27: search sanitize/1 delegates to QueryHelpers.sanitize_like/1 which
  escapes backslash first
- H30: JobCleanupTask no longer cancels jobs in "executing" state so
  in-flight polls complete naturally
2026-05-12 10:55:01 -05:00
810dafbf5e remove all fixed critical items from bugs.md, fix flaky MIB cache test 2026-05-12 10:35:52 -05:00
e96d077fdb remove C11 and C12 from bugs.md — not actual bugs 2026-05-12 10:24:15 -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
320ced2d23 fix: MIB archive bomb protection, atom exhaustion guard, and vendor caching
- Enforce upload size limit (100 MB), max extracted files (2000), max
  uncompressed size (500 MB), and reject symlinks in MIB archives
- Replace unbounded String.to_atom calls in SNMP tokenizer with
  safe_atom/1 that warns when approaching the atom table limit
- Cache MIB vendor listing with persistent_term, invalidated on
  upload/delete to avoid blocking synchronous Path.wildcard scans
2026-05-12 08:31:01 -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