- 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
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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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
- 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'
- 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
- 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