portal-mojo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- package/src/ui/ui.tsx +45 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Network security — perimeter control as ONE admin section under the
|
|
2
|
+
// Security navigation group, beside Security Operations, Bouncer, Devices &
|
|
3
|
+
// Logins and IP Intelligence.
|
|
4
|
+
//
|
|
5
|
+
// The section gate is the ANY-of UNION of the four child gates (the
|
|
6
|
+
// MONITORING_ADMIN_SECTION pattern); each route then carries its exact backend
|
|
7
|
+
// gate. A geofence-only operator (`sys.view_geofence` alone) therefore reaches
|
|
8
|
+
// Geofencing and is denied everywhere else — and the engine's landing redirect
|
|
9
|
+
// resolves to the first route they can actually see, so nobody lands on a
|
|
10
|
+
// denial page.
|
|
11
|
+
import type { AdminSection } from '../index';
|
|
12
|
+
import { BlockedIPsPage } from './BlockedIPsPage';
|
|
13
|
+
import { FirewallLogPage, FIREWALL_LOG_PERMS } from './FirewallLogPage';
|
|
14
|
+
import { IPSetsPage } from './IPSetsPage';
|
|
15
|
+
import { GeofencingPage } from './geofence/GeofencingPage';
|
|
16
|
+
import { GEOIP_VIEW_PERMS } from '../security/geoip';
|
|
17
|
+
import { IPSET_VIEW_PERMS } from './models';
|
|
18
|
+
import { GEOFENCE_VIEW_PERMS } from './geofence/geofence-data';
|
|
19
|
+
|
|
20
|
+
export * from './models';
|
|
21
|
+
export * from './BlockedIPsPage';
|
|
22
|
+
export * from './BlockedIpDetail';
|
|
23
|
+
export * from './FirewallLogPage';
|
|
24
|
+
export * from './IPSetsPage';
|
|
25
|
+
export * from './IPSetDetail';
|
|
26
|
+
export * from './IPSetEditor';
|
|
27
|
+
export * from './geofence/geofence-data';
|
|
28
|
+
export * from './geofence/RuleEditor';
|
|
29
|
+
export * from './geofence/PostureHeader';
|
|
30
|
+
export * from './geofence/RulesTab';
|
|
31
|
+
export * from './geofence/SimulatorTab';
|
|
32
|
+
export * from './geofence/BlocksTab';
|
|
33
|
+
export * from './geofence/ExemptionsTab';
|
|
34
|
+
export * from './geofence/GeofencingPage';
|
|
35
|
+
|
|
36
|
+
export const NETWORK_SECURITY_ADMIN_SECTION: AdminSection = {
|
|
37
|
+
id: 'network-security',
|
|
38
|
+
basePath: 'security/network',
|
|
39
|
+
title: 'Network Security',
|
|
40
|
+
icon: 'bi-hdd-network',
|
|
41
|
+
navigationGroup: 'security',
|
|
42
|
+
// The union of the four route gates — deduped so the sidebar's any-of
|
|
43
|
+
// check stays cheap and the verifier can compare it set-wise.
|
|
44
|
+
permissions: [...new Set([
|
|
45
|
+
...GEOIP_VIEW_PERMS,
|
|
46
|
+
...FIREWALL_LOG_PERMS,
|
|
47
|
+
...IPSET_VIEW_PERMS,
|
|
48
|
+
...GEOFENCE_VIEW_PERMS,
|
|
49
|
+
])],
|
|
50
|
+
routes: [
|
|
51
|
+
{
|
|
52
|
+
path: 'blocked-ips', label: 'Blocked IPs',
|
|
53
|
+
component: BlockedIPsPage, permissions: GEOIP_VIEW_PERMS,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: 'firewall-log', label: 'Firewall Log',
|
|
57
|
+
component: FirewallLogPage, permissions: FIREWALL_LOG_PERMS,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: 'ip-sets', label: 'IP Sets',
|
|
61
|
+
component: IPSetsPage, permissions: IPSET_VIEW_PERMS,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: 'geofencing', label: 'Geofencing',
|
|
65
|
+
component: GeofencingPage, permissions: GEOFENCE_VIEW_PERMS,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// Network security model layer — IP sets, the geofence config plane, the
|
|
2
|
+
// geofence evidence projection, and the firewall-log payload contract.
|
|
3
|
+
//
|
|
4
|
+
// Ported from web-mojo `admin/models/IPSet.js` + `admin/security/IPSetView.js`
|
|
5
|
+
// + the five `admin/security/geofence/*` views (all read in full 2026-08-06),
|
|
6
|
+
// reconciled against the authoritative django-mojo sources:
|
|
7
|
+
// · `mojo/apps/incident/models/ipset.py` + `rest/ipset.py`
|
|
8
|
+
// · `mojo/apps/account/rest/geofence.py`
|
|
9
|
+
// · `mojo/apps/account/services/geofence/{engine,evidence}.py`
|
|
10
|
+
// · `mojo/apps/account/models/geolocated_ip.py` (firewall Log rows)
|
|
11
|
+
// · `mojo/apps/metrics/rest/{categories,helpers}.py`
|
|
12
|
+
//
|
|
13
|
+
// `/api/system/geoip` is NOT defined here. #1291 owns it
|
|
14
|
+
// (`admin/security/geoip/models.ts`) — one endpoint, one defineModel, one
|
|
15
|
+
// cache key. Blocked IPs is a projection over that model.
|
|
16
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
17
|
+
import {
|
|
18
|
+
MojoError, defineModel, mojoCall, mojoList, mojoMetrics, useCan,
|
|
19
|
+
type Envelope, type Params,
|
|
20
|
+
} from '../../client';
|
|
21
|
+
import { SECURITY_DELETE_PERMS, SECURITY_MANAGE_PERMS, SECURITY_VIEW_PERMS } from '../security-permissions';
|
|
22
|
+
import { sanitizeEventRow } from '../incidents/sanitize';
|
|
23
|
+
import type { EventRow } from '../incidents/models';
|
|
24
|
+
import { GEOFENCE_VIEW_PERMS, SECURITY_EVENTS_PERMS } from './geofence/geofence-data';
|
|
25
|
+
import type {
|
|
26
|
+
AllowlistPayload, BypassHoldersPayload, GeoDecision, GeoRulesConfig, GeofenceRule,
|
|
27
|
+
} from './geofence/geofence-data';
|
|
28
|
+
|
|
29
|
+
// ── Permissions (backend-derived, `sys.`-pinned, fail-closed) ─────────
|
|
30
|
+
|
|
31
|
+
/** `IPSet.RestMeta.VIEW_PERMS = ["view_security","security"]`. */
|
|
32
|
+
export const IPSET_VIEW_PERMS = SECURITY_VIEW_PERMS;
|
|
33
|
+
/** `IPSet.RestMeta.SAVE_PERMS = ["manage_security","security"]`. */
|
|
34
|
+
export const IPSET_MANAGE_PERMS = SECURITY_MANAGE_PERMS;
|
|
35
|
+
/** `IPSet.RestMeta.DELETE_PERMS = ["manage_security"]` — deliberately narrower. */
|
|
36
|
+
export const IPSET_DELETE_PERMS = SECURITY_DELETE_PERMS;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `metrics/rest/helpers.check_view_permissions`: for `account == "global"` the
|
|
40
|
+
* required any-of list is exactly `["view_metrics","metrics"]`, checked against
|
|
41
|
+
* the caller's GLOBAL permissions. Every geofence metrics read is gated on this
|
|
42
|
+
* so a geofence-only operator issues no denied request.
|
|
43
|
+
*/
|
|
44
|
+
export const METRICS_GLOBAL_VIEW_PERMS = ['sys.view_metrics', 'sys.metrics'];
|
|
45
|
+
|
|
46
|
+
// ── IPSet ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The `default` graph: `{"exclude": ["data", "source_key"]}`.
|
|
50
|
+
* `source_key` (the AbuseIPDB API key) is excluded from BOTH graphs and an
|
|
51
|
+
* unknown graph name falls back to `default`, so it can never be read back —
|
|
52
|
+
* it is deliberately absent from this type.
|
|
53
|
+
*/
|
|
54
|
+
export interface IPSetRow {
|
|
55
|
+
id: number;
|
|
56
|
+
created: number;
|
|
57
|
+
modified: number;
|
|
58
|
+
name: string;
|
|
59
|
+
kind: string;
|
|
60
|
+
description: string | null;
|
|
61
|
+
source: string;
|
|
62
|
+
source_url: string | null;
|
|
63
|
+
is_enabled: boolean;
|
|
64
|
+
cidr_count: number;
|
|
65
|
+
last_synced: number | null;
|
|
66
|
+
sync_error: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The `detailed` graph adds `data`: the CIDR list, one entry per line. */
|
|
70
|
+
export interface IPSetDetailedRow extends IPSetRow {
|
|
71
|
+
data: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `IPSet.KIND_CHOICES`, with the source's explanatory create-form labels. */
|
|
75
|
+
export const IPSET_KIND_OPTIONS = [
|
|
76
|
+
{ value: 'country', label: 'Country — block all traffic from a country' },
|
|
77
|
+
{ value: 'abuse', label: 'Abuse feed — import known attacker IPs' },
|
|
78
|
+
{ value: 'datacenter', label: 'Datacenter — block datacenter/hosting ranges' },
|
|
79
|
+
{ value: 'custom', label: 'Custom — define your own CIDR list' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/** The same four values with short labels, for badges and filters. */
|
|
83
|
+
export const IPSET_KIND_BADGE_OPTIONS = [
|
|
84
|
+
{ value: 'country', label: 'Country' },
|
|
85
|
+
{ value: 'abuse', label: 'Abuse feed' },
|
|
86
|
+
{ value: 'datacenter', label: 'Datacenter' },
|
|
87
|
+
{ value: 'custom', label: 'Custom' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* BACKEND CORRECTION. `IPSet.SOURCE_CHOICES` has FIVE members; web-mojo listed
|
|
92
|
+
* three, so `tor` and `blocklist_de` rows — the two cache-only threat lists —
|
|
93
|
+
* rendered a raw slug in the Source column and could not be filtered at all.
|
|
94
|
+
*/
|
|
95
|
+
export const IPSET_SOURCE_OPTIONS = [
|
|
96
|
+
{ value: 'ipdeny', label: 'IPDeny (country zones)' },
|
|
97
|
+
{ value: 'abuseipdb', label: 'AbuseIPDB' },
|
|
98
|
+
{ value: 'tor', label: 'Tor exit list' },
|
|
99
|
+
{ value: 'blocklist_de', label: 'blocklist.de' },
|
|
100
|
+
{ value: 'manual', label: 'Manual' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `IPSet.THREAT_CACHE_SETS` — sets that exist to feed geoip THREAT DETECTION,
|
|
105
|
+
* not the kernel firewall. `on_action_enable` refuses them outright, and
|
|
106
|
+
* `sync()` silently no-ops for them.
|
|
107
|
+
*/
|
|
108
|
+
export const IPSET_CACHE_ONLY_NAMES: readonly string[] = ['tor_exits', 'blocklist_de'];
|
|
109
|
+
|
|
110
|
+
/** The model's own words for why enabling a cache-only set is refused. */
|
|
111
|
+
export const IPSET_CACHE_ONLY_HELP =
|
|
112
|
+
'geoip detection cache — enabling would kernel-block every listed IP fleet-wide';
|
|
113
|
+
|
|
114
|
+
export function isCacheOnlyIPSet(row: Pick<IPSetRow, 'name'> | null | undefined): boolean {
|
|
115
|
+
return row != null && IPSET_CACHE_ONLY_NAMES.includes(row.name);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* web-mojo's `CommonBlockCountries` — pinned to the top of the country picker
|
|
120
|
+
* so the twenty codes an operator actually reaches for are one scroll away.
|
|
121
|
+
* The FULL ISO2 list follows (from #1426's centroid table); the source offered
|
|
122
|
+
* only these twenty, so no other country could be blocked from the UI at all.
|
|
123
|
+
*/
|
|
124
|
+
export const IPSET_COMMON_BLOCK_COUNTRIES: readonly string[] = [
|
|
125
|
+
'CN', 'RU', 'KP', 'IR', 'NG', 'RO', 'BR', 'IN', 'PK', 'ID',
|
|
126
|
+
'VN', 'UA', 'TH', 'PH', 'BD', 'EG', 'TR', 'MX', 'AR', 'CO',
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const COMMON_PARAMS = new Set(['start', 'size', 'sort', 'search', 'dr_field', 'dr_start', 'dr_end']);
|
|
130
|
+
|
|
131
|
+
/** Filterable fields. `graph` is pinned to `default`, so `data` can never
|
|
132
|
+
* ride a list response into the persistent query cache. */
|
|
133
|
+
const IPSET_FILTERS = new Set([
|
|
134
|
+
'id', 'id__in', 'name', 'name__icontains', 'description__icontains',
|
|
135
|
+
'kind', 'kind__in', 'source', 'source__in', 'is_enabled',
|
|
136
|
+
'cidr_count__gte', 'cidr_count__lte',
|
|
137
|
+
'last_synced__gte', 'last_synced__lte', 'sync_error__isnull',
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
export function normalizeIPSetListParams(params: Params): Params {
|
|
141
|
+
const out: Params = { graph: 'default' };
|
|
142
|
+
for (const [key, value] of Object.entries(params)) {
|
|
143
|
+
if ((COMMON_PARAMS.has(key) || IPSET_FILTERS.has(key)) && value != null && value !== '') out[key] = value;
|
|
144
|
+
}
|
|
145
|
+
out.sort = String(out.sort ?? 'name');
|
|
146
|
+
if (out.dr_start != null || out.dr_end != null) out.dr_field = String(out.dr_field ?? 'last_synced');
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* `IPSet.RestMeta.POST_SAVE_ACTIONS = ["sync","enable","disable","refresh_source"]`.
|
|
152
|
+
* All four handlers return nothing, so `on_rest_save` answers with the
|
|
153
|
+
* refreshed record — `response: 'row'` (the default) is correct for all of
|
|
154
|
+
* them and the UI re-reads state rather than optimistically toggling.
|
|
155
|
+
*
|
|
156
|
+
* `enable` is the ONLY path that runs the cache-only rejection
|
|
157
|
+
* (`on_action_enable` raises a 400 naming the set). `is_enabled` is therefore
|
|
158
|
+
* NEVER written as a plain field anywhere in this module — web-mojo's create
|
|
159
|
+
* default and edit switch both did, producing a row that reads "Enabled" and
|
|
160
|
+
* silently never syncs. Direct #1097 lineage.
|
|
161
|
+
*/
|
|
162
|
+
export const IPSetModel = defineModel<IPSetRow>({
|
|
163
|
+
name: 'ipset',
|
|
164
|
+
endpoint: '/api/incident/ipset',
|
|
165
|
+
permissions: {
|
|
166
|
+
view: IPSET_VIEW_PERMS,
|
|
167
|
+
manage: IPSET_MANAGE_PERMS,
|
|
168
|
+
delete: IPSET_DELETE_PERMS,
|
|
169
|
+
},
|
|
170
|
+
actions: {
|
|
171
|
+
sync: { permissions: IPSET_MANAGE_PERMS },
|
|
172
|
+
enable: { permissions: IPSET_MANAGE_PERMS },
|
|
173
|
+
disable: { permissions: IPSET_MANAGE_PERMS },
|
|
174
|
+
refresh_source: { permissions: IPSET_MANAGE_PERMS },
|
|
175
|
+
},
|
|
176
|
+
normalizeListParams: normalizeIPSetListParams,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/** Graph is part of the key so a sparse `default` row cannot satisfy detail. */
|
|
180
|
+
export function ipSetCidrKey(id: number) {
|
|
181
|
+
return [IPSetModel.endpoint, 'one', id, { graph: 'detailed' }] as const;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface IPSetCidrData {
|
|
185
|
+
/** The stored blob, verbatim. */
|
|
186
|
+
text: string;
|
|
187
|
+
/** Lines that are neither blank nor comments — the HONEST count. */
|
|
188
|
+
lines: string[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The only reader of `graph=detailed`, i.e. the only path that sees `data`.
|
|
193
|
+
* `gcTime: 0` keeps a multi-megabyte country zone out of the persistent cache.
|
|
194
|
+
*/
|
|
195
|
+
export function useIPSetCidrData(id: number | null, enabled: boolean) {
|
|
196
|
+
return useQuery({
|
|
197
|
+
queryKey: id == null
|
|
198
|
+
? [IPSetModel.endpoint, 'one', null, { graph: 'detailed' }] as const
|
|
199
|
+
: ipSetCidrKey(id),
|
|
200
|
+
queryFn: async (): Promise<IPSetCidrData> => {
|
|
201
|
+
const body = await mojoCall(`${IPSetModel.endpoint}/${id!}`, { params: { graph: 'detailed' } });
|
|
202
|
+
const raw = body.data as IPSetDetailedRow | null;
|
|
203
|
+
const text = typeof raw?.data === 'string' ? raw.data : '';
|
|
204
|
+
return { text, lines: parseCidrLines(text) };
|
|
205
|
+
},
|
|
206
|
+
enabled: enabled && id != null,
|
|
207
|
+
gcTime: 0,
|
|
208
|
+
staleTime: 0,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Split a textarea blob into the CIDR list the backend expects.
|
|
214
|
+
*
|
|
215
|
+
* BACKEND TRAP. `data` has a `set_data()` setter, and `on_rest_save_field`
|
|
216
|
+
* prefers `set_<key>` over a plain assignment — so a posted `data` runs
|
|
217
|
+
* `"\n".join(value)`. Posting a STRING therefore interleaves a newline between
|
|
218
|
+
* every CHARACTER and sets `cidr_count` to the character count. The value must
|
|
219
|
+
* be posted as a JSON LIST. web-mojo posted the raw textarea string.
|
|
220
|
+
*
|
|
221
|
+
* Blank lines and `#` comments are dropped (the source's own help text says
|
|
222
|
+
* they are ignored — but the backend does not drop them, so we do).
|
|
223
|
+
*/
|
|
224
|
+
export function parseCidrLines(text: string | null | undefined): string[] {
|
|
225
|
+
return String(text ?? '')
|
|
226
|
+
.split(/\r?\n/)
|
|
227
|
+
.map((line) => line.trim())
|
|
228
|
+
.filter((line) => line !== '' && !line.startsWith('#'));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Geofence evidence projection ──────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A SECOND `defineModel` on `/api/incident/event`, deliberately.
|
|
235
|
+
*
|
|
236
|
+
* The shared `EventModel` (admin/incidents) allowlists a filter set built for
|
|
237
|
+
* the security-events page; `metadata__reason`, `metadata__geofence_scope` and
|
|
238
|
+
* `metadata__region_code` are not in it and would be silently DROPPED, which
|
|
239
|
+
* is the worst possible failure for a filter (the table would answer with
|
|
240
|
+
* everything and look correct). A local definition keeps
|
|
241
|
+
* `admin/incidents/models.ts` untouched.
|
|
242
|
+
*
|
|
243
|
+
* The two models share `endpoint`, and `defineModel`'s invalidation root is
|
|
244
|
+
* `[endpoint]` — so an invalidation from either reaches both, and their query
|
|
245
|
+
* keys never collide because their params never overlap.
|
|
246
|
+
*/
|
|
247
|
+
const GEOFENCE_EVENT_FILTERS = new Set([
|
|
248
|
+
'id', 'category', 'category__in', 'level', 'level__gte', 'level__lte',
|
|
249
|
+
'source_ip', 'source_ip__icontains', 'scope',
|
|
250
|
+
// The INDEXED top-level column. `Event.sync_metadata` fills it from the
|
|
251
|
+
// geolocated source IP, so it is populated for geofence blocks and is the
|
|
252
|
+
// right thing to filter on — the metadata copy is not indexed.
|
|
253
|
+
'country_code', 'country_code__in',
|
|
254
|
+
'created__gte', 'created__lte',
|
|
255
|
+
// Written by services/geofence/evidence.py. `geofence_scope` — NOT
|
|
256
|
+
// `scope`: the reporter never passes the reporter's `scope=` argument, so
|
|
257
|
+
// the top-level Event column stays "global" for every geofence row.
|
|
258
|
+
// web-mojo read `metadata.scope` and the column was always "—".
|
|
259
|
+
'metadata__reason', 'metadata__geofence_scope', 'metadata__region_code',
|
|
260
|
+
'metadata__rule_level', 'metadata__allowlist_source', 'metadata__username',
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
export function normalizeGeofenceEventListParams(params: Params): Params {
|
|
264
|
+
const out: Params = { graph: 'default' };
|
|
265
|
+
for (const [key, value] of Object.entries(params)) {
|
|
266
|
+
if ((COMMON_PARAMS.has(key) || GEOFENCE_EVENT_FILTERS.has(key)) && value != null && value !== '') out[key] = value;
|
|
267
|
+
}
|
|
268
|
+
// Day grouping requires chronological contiguity.
|
|
269
|
+
const sort = String(out.sort ?? '-created');
|
|
270
|
+
out.sort = sort === 'created' || sort === '-created' ? sort : '-created';
|
|
271
|
+
if (out.dr_start != null || out.dr_end != null) out.dr_field = String(out.dr_field ?? 'created');
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const GeofenceEventModel = defineModel<EventRow>({
|
|
276
|
+
name: 'geofence-event',
|
|
277
|
+
endpoint: '/api/incident/event',
|
|
278
|
+
permissions: { view: SECURITY_EVENTS_PERMS },
|
|
279
|
+
normalizeListParams: normalizeGeofenceEventListParams,
|
|
280
|
+
sanitizeRow: sanitizeEventRow,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export const GEOFENCE_EVENT_CATEGORIES = {
|
|
284
|
+
block: 'geofence_block',
|
|
285
|
+
exempt: 'geofence_exempt',
|
|
286
|
+
config: 'geofence_config',
|
|
287
|
+
} as const;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* `evidence._block_level` (evidence.py:150-158), labelled from the module's
|
|
291
|
+
* own docstring. These are the only four levels a geofence block can carry.
|
|
292
|
+
*/
|
|
293
|
+
export const GEOFENCE_LEVEL_OPTIONS = [
|
|
294
|
+
{ value: '3', label: 'Jurisdiction block (3)' },
|
|
295
|
+
{ value: '5', label: 'Abuse / fail-closed / strict (5)' },
|
|
296
|
+
{ value: '6', label: 'Fail-open pass-through (6)' },
|
|
297
|
+
{ value: '7', label: 'Rule error (7)' },
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The "who" on a `geofence_config` event.
|
|
302
|
+
*
|
|
303
|
+
* BACKEND CORRECTION. `evidence.report_config_change` writes `changed_by` (a
|
|
304
|
+
* username). `reporter._create_event_dict` separately writes `user_name` (the
|
|
305
|
+
* display name) for any authenticated request. web-mojo read
|
|
306
|
+
* `metadata.username`, which NEITHER writes — so the Last-change chip and the
|
|
307
|
+
* change-history "Who" column always rendered blank.
|
|
308
|
+
*/
|
|
309
|
+
export function configChangedBy(metadata: Record<string, unknown> | null | undefined): string {
|
|
310
|
+
const changedBy = metadata?.changed_by;
|
|
311
|
+
if (typeof changedBy === 'string' && changedBy) return changedBy;
|
|
312
|
+
const userName = metadata?.user_name;
|
|
313
|
+
if (typeof userName === 'string' && userName) return userName;
|
|
314
|
+
return '';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Firewall log ──────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/** The exact `kind` values `GeoLocatedIP` writes. */
|
|
320
|
+
export const FIREWALL_LOG_KINDS = {
|
|
321
|
+
block: 'firewall:block',
|
|
322
|
+
unblock: 'firewall:unblock',
|
|
323
|
+
whitelist: 'firewall:whitelist',
|
|
324
|
+
unwhitelist: 'firewall:unwhitelist',
|
|
325
|
+
} as const;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The parsed firewall payload.
|
|
329
|
+
*
|
|
330
|
+
* COLUMN HONESTY. `Log.path` is the ADMIN's HTTP request path and `Log.ip` is
|
|
331
|
+
* the ADMIN's IP — both come from the ambient request, not from the record
|
|
332
|
+
* being blocked. The blocked address lives only in `payload.ip`. web-mojo
|
|
333
|
+
* labelled `path` as "IP / Path", which was never either.
|
|
334
|
+
*
|
|
335
|
+
* The payload shape is NOT uniform across the four kinds:
|
|
336
|
+
* block {ip, reason, ttl, blocked_until, block_count, trigger}
|
|
337
|
+
* unblock {ip, reason, trigger}
|
|
338
|
+
* whitelist {ip, reason, until, was_blocked, trigger}
|
|
339
|
+
* unwhitelist {ip, trigger}
|
|
340
|
+
*/
|
|
341
|
+
export interface FirewallPayload {
|
|
342
|
+
ip?: string;
|
|
343
|
+
reason?: string;
|
|
344
|
+
ttl?: number | null;
|
|
345
|
+
blocked_until?: string | null;
|
|
346
|
+
block_count?: number;
|
|
347
|
+
until?: string | null;
|
|
348
|
+
was_blocked?: boolean;
|
|
349
|
+
trigger?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function firewallPayloadOf(raw: string | null | undefined): FirewallPayload | null {
|
|
353
|
+
if (!raw) return null;
|
|
354
|
+
try {
|
|
355
|
+
const parsed: unknown = JSON.parse(raw);
|
|
356
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
357
|
+
? parsed as FirewallPayload
|
|
358
|
+
: null;
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Geofence config plane ─────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
export const GEO_RULES_ENDPOINT = '/api/geo/rules';
|
|
367
|
+
export const GEO_SIMULATE_ENDPOINT = '/api/geo/simulate';
|
|
368
|
+
export const GEO_ALLOWLIST_ENDPOINT = '/api/geo/allowlist';
|
|
369
|
+
export const GEO_BYPASS_ENDPOINT = '/api/geo/bypass_holders';
|
|
370
|
+
|
|
371
|
+
export const geoKeys = {
|
|
372
|
+
rules: (groupUuid?: string | null) => [GEO_RULES_ENDPOINT, groupUuid ?? null] as const,
|
|
373
|
+
allowlist: () => [GEO_ALLOWLIST_ENDPOINT] as const,
|
|
374
|
+
bypass: () => [GEO_BYPASS_ENDPOINT] as const,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* `/api/geo/*` is simply NOT REGISTERED on a backend older than the config
|
|
379
|
+
* plane, so the wire answer is a 404 — not an error envelope. Every geofence
|
|
380
|
+
* surface branches on this and explains; any other status shows the server's
|
|
381
|
+
* own message.
|
|
382
|
+
*/
|
|
383
|
+
export function isGeofenceApiMissing(error: unknown): boolean {
|
|
384
|
+
return error instanceof MojoError && error.status === 404;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export const GEOFENCE_API_MISSING_MESSAGE =
|
|
388
|
+
'This server does not expose the geofence administration API. It was added to django-mojo in v1.2.42; older deployments simply do not register the /api/geo routes.';
|
|
389
|
+
|
|
390
|
+
/** GET /api/geo/rules. One query feeds the posture header and all four tabs. */
|
|
391
|
+
export function useGeoConfig(groupUuid?: string | null) {
|
|
392
|
+
const { can } = useCan(GEOFENCE_VIEW_PERMS);
|
|
393
|
+
return useQuery({
|
|
394
|
+
queryKey: geoKeys.rules(groupUuid),
|
|
395
|
+
queryFn: async (): Promise<GeoRulesConfig> => {
|
|
396
|
+
const body = await mojoCall(GEO_RULES_ENDPOINT, {
|
|
397
|
+
params: groupUuid ? { group_uuid: groupUuid } : {},
|
|
398
|
+
});
|
|
399
|
+
return (body.data ?? {}) as GeoRulesConfig;
|
|
400
|
+
},
|
|
401
|
+
enabled: can,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function useGeoAllowlist() {
|
|
406
|
+
const { can } = useCan(GEOFENCE_VIEW_PERMS);
|
|
407
|
+
return useQuery({
|
|
408
|
+
queryKey: geoKeys.allowlist(),
|
|
409
|
+
queryFn: async (): Promise<AllowlistPayload> => {
|
|
410
|
+
const body = await mojoCall(GEO_ALLOWLIST_ENDPOINT);
|
|
411
|
+
return (body.data ?? {}) as AllowlistPayload;
|
|
412
|
+
},
|
|
413
|
+
enabled: can,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function useBypassHolders() {
|
|
418
|
+
const { can } = useCan(GEOFENCE_VIEW_PERMS);
|
|
419
|
+
return useQuery({
|
|
420
|
+
queryKey: geoKeys.bypass(),
|
|
421
|
+
queryFn: async (): Promise<BypassHoldersPayload> => {
|
|
422
|
+
const body = await mojoCall(GEO_BYPASS_ENDPOINT);
|
|
423
|
+
return (body.data ?? {}) as BypassHoldersPayload;
|
|
424
|
+
},
|
|
425
|
+
enabled: can,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* `POST /api/geo/rules {rule}` — a FULL REPLACE that takes effect fleet-wide
|
|
431
|
+
* immediately and invalidates cached decisions. Never a merge, never the
|
|
432
|
+
* generic Settings editor (which would bypass `validate_rule` entirely).
|
|
433
|
+
* A DSL failure comes back as the validator's own human-readable message.
|
|
434
|
+
*/
|
|
435
|
+
export function useSaveGeoRules() {
|
|
436
|
+
const qc = useQueryClient();
|
|
437
|
+
return useMutation({
|
|
438
|
+
mutationFn: async (rule: GeofenceRule) => {
|
|
439
|
+
const body = await mojoCall(GEO_RULES_ENDPOINT, { method: 'POST', body: { rule } });
|
|
440
|
+
return (body.data ?? {}) as { rule?: GeofenceRule; source?: string; modified?: string };
|
|
441
|
+
},
|
|
442
|
+
onSuccess: () => { void qc.invalidateQueries({ queryKey: [GEO_RULES_ENDPOINT] }); },
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** `DELETE /api/geo/rules` — drop the portal override; the deploy file (or
|
|
447
|
+
* nothing) becomes effective again. */
|
|
448
|
+
export function useRemoveGeoRulesOverride() {
|
|
449
|
+
const qc = useQueryClient();
|
|
450
|
+
return useMutation({
|
|
451
|
+
mutationFn: async () => {
|
|
452
|
+
const body = await mojoCall(GEO_RULES_ENDPOINT, { method: 'DELETE' });
|
|
453
|
+
return (body.data ?? {}) as { removed?: boolean };
|
|
454
|
+
},
|
|
455
|
+
onSuccess: () => { void qc.invalidateQueries({ queryKey: [GEO_RULES_ENDPOINT] }); },
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** `POST /api/geo/simulate` — never cached, never logged, never enforced. */
|
|
460
|
+
export function useGeoSimulate() {
|
|
461
|
+
return useMutation({
|
|
462
|
+
mutationFn: async (body: Record<string, unknown>): Promise<GeoDecision> => {
|
|
463
|
+
const response = await mojoCall(GEO_SIMULATE_ENDPOINT, { method: 'POST', body });
|
|
464
|
+
return (response.data ?? {}) as GeoDecision;
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export class AllowlistRaceError extends Error {
|
|
470
|
+
constructor() {
|
|
471
|
+
super('The exemption list changed on the server while this form was open. Nothing was saved — reopen the list and try again.');
|
|
472
|
+
this.name = 'AllowlistRaceError';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The wire shape a `POST /api/geo/allowlist` entry may carry: keys are
|
|
477
|
+
* validated against `{cidr, ip, reason, until}` server-side. */
|
|
478
|
+
export interface AllowlistWriteEntry {
|
|
479
|
+
cidr: string;
|
|
480
|
+
reason?: string;
|
|
481
|
+
until?: string;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export interface SaveAllowlistVars {
|
|
485
|
+
/** The full replacement list. */
|
|
486
|
+
entries: AllowlistWriteEntry[];
|
|
487
|
+
/**
|
|
488
|
+
* The `setting` array exactly as it was read when the form opened. The
|
|
489
|
+
* server is refetched and compared against this before the POST.
|
|
490
|
+
*/
|
|
491
|
+
baseline: unknown;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* `POST /api/geo/allowlist {entries}` is a FULL REPLACE of the
|
|
496
|
+
* GEOFENCE_ALLOWLIST setting, so a stale local copy silently deletes another
|
|
497
|
+
* operator's concurrent entry. Every write therefore refetches first and
|
|
498
|
+
* ABORTS when the server list has moved. web-mojo posted from possibly-stale
|
|
499
|
+
* local state with no guard at all.
|
|
500
|
+
*
|
|
501
|
+
* An empty list is a legitimate CLEAR, not "no change" — the caller confirms
|
|
502
|
+
* it explicitly.
|
|
503
|
+
*/
|
|
504
|
+
export function useSaveGeoAllowlist() {
|
|
505
|
+
const qc = useQueryClient();
|
|
506
|
+
return useMutation({
|
|
507
|
+
mutationFn: async ({ entries, baseline }: SaveAllowlistVars) => {
|
|
508
|
+
const current = await mojoCall(GEO_ALLOWLIST_ENDPOINT);
|
|
509
|
+
const currentSetting = ((current.data ?? {}) as AllowlistPayload).setting ?? [];
|
|
510
|
+
if (JSON.stringify(currentSetting) !== JSON.stringify(baseline)) throw new AllowlistRaceError();
|
|
511
|
+
const body = await mojoCall(GEO_ALLOWLIST_ENDPOINT, { method: 'POST', body: { entries } });
|
|
512
|
+
return (body.data ?? {}) as { entries?: unknown[] };
|
|
513
|
+
},
|
|
514
|
+
onSuccess: () => {
|
|
515
|
+
void qc.invalidateQueries({ queryKey: [GEO_ALLOWLIST_ENDPOINT] });
|
|
516
|
+
void qc.invalidateQueries({ queryKey: [GEO_RULES_ENDPOINT] });
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ── Geofence country totals (server-derived, no client row work) ──────
|
|
522
|
+
|
|
523
|
+
const CATEGORY_SLUGS_ENDPOINT = '/api/metrics/category_slugs';
|
|
524
|
+
const COUNTRY_SLUG_PREFIX = 'geofence:blocks:country:';
|
|
525
|
+
|
|
526
|
+
type CategorySlugsEnvelope = Envelope & { slugs?: unknown };
|
|
527
|
+
|
|
528
|
+
export interface CountryTotal {
|
|
529
|
+
country_code: string;
|
|
530
|
+
total: number;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* "Top blocked countries", derived entirely server-side:
|
|
535
|
+
* 1. `GET /api/metrics/category_slugs?category=geofence&account=global`
|
|
536
|
+
* — a FLAT envelope (`{slugs, category, account, status}`); this route
|
|
537
|
+
* returns a raw JsonResponse and does NOT nest under `data`;
|
|
538
|
+
* 2. filter to the `geofence:blocks:country:*` family;
|
|
539
|
+
* 3. ONE `/api/metrics/fetch` over that family, summed per slug.
|
|
540
|
+
*
|
|
541
|
+
* This is never computed from the visible page of events — that would lie
|
|
542
|
+
* about totals whenever the page did not cover the window. It is also exactly
|
|
543
|
+
* the dataset a choropleth would consume, which is why it ships as the
|
|
544
|
+
* geographic answer here (the map itself is #1426's, reserved).
|
|
545
|
+
*
|
|
546
|
+
* `get_category_slugs` returns a SET, so the wire order is nondeterministic;
|
|
547
|
+
* the sort below is the client's, over server-computed totals.
|
|
548
|
+
*/
|
|
549
|
+
export function useGeofenceCountryTotals(days = 30, enabled = true) {
|
|
550
|
+
const { can } = useCan(METRICS_GLOBAL_VIEW_PERMS);
|
|
551
|
+
return useQuery({
|
|
552
|
+
queryKey: ['geofence-country-totals', days] as const,
|
|
553
|
+
queryFn: async (): Promise<CountryTotal[]> => {
|
|
554
|
+
const body = await mojoCall(CATEGORY_SLUGS_ENDPOINT, {
|
|
555
|
+
params: { category: 'geofence', account: 'global' },
|
|
556
|
+
}) as CategorySlugsEnvelope;
|
|
557
|
+
const slugs = Array.isArray(body.slugs) ? body.slugs.map(String) : [];
|
|
558
|
+
const countrySlugs = slugs.filter((slug) => slug.startsWith(COUNTRY_SLUG_PREFIX)).sort();
|
|
559
|
+
if (countrySlugs.length === 0) return [];
|
|
560
|
+
const metrics = await mojoMetrics({
|
|
561
|
+
slugs: countrySlugs.join(','),
|
|
562
|
+
account: 'global',
|
|
563
|
+
granularity: 'days',
|
|
564
|
+
range: days > 7 ? '30d' : '7d',
|
|
565
|
+
});
|
|
566
|
+
return metrics.datasets
|
|
567
|
+
.map((series) => ({
|
|
568
|
+
country_code: series.label.slice(COUNTRY_SLUG_PREFIX.length).toUpperCase(),
|
|
569
|
+
total: series.data.reduce((sum, value) => sum + (Number(value) || 0), 0),
|
|
570
|
+
}))
|
|
571
|
+
.filter((row) => row.total > 0)
|
|
572
|
+
.sort((a, b) => b.total - a.total || a.country_code.localeCompare(b.country_code));
|
|
573
|
+
},
|
|
574
|
+
enabled: can && enabled,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ── Convenience: the geoip whitelist leg of the Exemptions tab ────────
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Resolve an address to its GeoLocatedIP id so the `whitelist` action can be
|
|
582
|
+
* addressed. `/api/system/geoip/lookup` CREATES the record when it is absent
|
|
583
|
+
* and refreshes it when expired, behind a 30-per-IP server rate limit — which
|
|
584
|
+
* is exactly right here (an operator is whitelisting an address that may never
|
|
585
|
+
* have been seen), and is why this is an explicit action and never a query.
|
|
586
|
+
*/
|
|
587
|
+
export async function resolveGeoIpIdForAddress(endpoint: string, ip: string): Promise<number> {
|
|
588
|
+
const body = await mojoCall(`${endpoint}/lookup`, { params: { ip, auto_refresh: '1' } });
|
|
589
|
+
const row = body.data as { id?: unknown } | null;
|
|
590
|
+
if (row == null || typeof row.id !== 'number') {
|
|
591
|
+
throw new MojoError(`Could not resolve ${ip} to a GeoIP record`, 400);
|
|
592
|
+
}
|
|
593
|
+
return row.id;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* The CHEAP direction: a filtered read of the cache table. Used when the row
|
|
598
|
+
* is known to exist (removing a whitelist), so removal never costs a provider
|
|
599
|
+
* call and never consumes the `/lookup` rate limit.
|
|
600
|
+
*/
|
|
601
|
+
export async function findGeoIpIdByAddress(endpoint: string, ip: string): Promise<number> {
|
|
602
|
+
const page = await mojoList<{ id: number }>(endpoint, { ip_address: ip, size: 1, graph: 'default' });
|
|
603
|
+
const id = page.rows[0]?.id;
|
|
604
|
+
if (typeof id !== 'number') throw new MojoError(`No GeoIP record cached for ${ip}`, 404);
|
|
605
|
+
return id;
|
|
606
|
+
}
|
|
607
|
+
|