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,199 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
defineModel, mojoCall,
|
|
4
|
+
type Params,
|
|
5
|
+
} from '../../client';
|
|
6
|
+
import { SECURITY_VIEW_PERMS } from '../security-permissions';
|
|
7
|
+
import { sanitizeIncidentRow } from '../incidents/sanitize';
|
|
8
|
+
|
|
9
|
+
/** Bouncer data is global. `sys.` prevents an active group grant from opening it. */
|
|
10
|
+
export const BOUNCER_VIEW_PERMS = [
|
|
11
|
+
'sys.manage_users',
|
|
12
|
+
'sys.view_security',
|
|
13
|
+
'sys.manage_security',
|
|
14
|
+
'sys.security',
|
|
15
|
+
'sys.users',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const BOUNCER_MANAGE_PERMS = [
|
|
19
|
+
'sys.manage_users',
|
|
20
|
+
'sys.manage_security',
|
|
21
|
+
'sys.security',
|
|
22
|
+
'sys.users',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Incident rows have their own, narrower model gate. */
|
|
26
|
+
export const BOUNCER_INCIDENT_VIEW_PERMS = SECURITY_VIEW_PERMS;
|
|
27
|
+
|
|
28
|
+
export type BouncerStage = 'assess' | 'submit' | 'event';
|
|
29
|
+
export type BouncerDecision = 'allow' | 'monitor' | 'block' | 'log';
|
|
30
|
+
export type BouncerRiskTier = 'unknown' | 'low' | 'medium' | 'high' | 'blocked';
|
|
31
|
+
export type BotSignatureType =
|
|
32
|
+
| 'ip'
|
|
33
|
+
| 'subnet_24'
|
|
34
|
+
| 'subnet_16'
|
|
35
|
+
| 'user_agent'
|
|
36
|
+
| 'fingerprint'
|
|
37
|
+
| 'signal_set';
|
|
38
|
+
export type BotSignatureSource = 'auto' | 'manual';
|
|
39
|
+
|
|
40
|
+
export interface BouncerGeoIp {
|
|
41
|
+
id: number;
|
|
42
|
+
ip_address: string;
|
|
43
|
+
country_code: string | null;
|
|
44
|
+
country_name: string | null;
|
|
45
|
+
region: string | null;
|
|
46
|
+
city: string | null;
|
|
47
|
+
asn: string | null;
|
|
48
|
+
asn_org: string | null;
|
|
49
|
+
isp: string | null;
|
|
50
|
+
threat_level: string | null;
|
|
51
|
+
is_tor: boolean;
|
|
52
|
+
is_vpn: boolean;
|
|
53
|
+
is_proxy: boolean;
|
|
54
|
+
is_cloud: boolean;
|
|
55
|
+
is_datacenter: boolean;
|
|
56
|
+
is_known_attacker: boolean;
|
|
57
|
+
is_known_abuser: boolean;
|
|
58
|
+
[field: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BouncerDeviceRow {
|
|
62
|
+
id: number;
|
|
63
|
+
muid: string;
|
|
64
|
+
duid: string;
|
|
65
|
+
msid?: string;
|
|
66
|
+
fingerprint_id?: string | null;
|
|
67
|
+
risk_tier: BouncerRiskTier;
|
|
68
|
+
event_count: number;
|
|
69
|
+
block_count: number;
|
|
70
|
+
last_seen_ip: string | null;
|
|
71
|
+
linked_muids?: string[];
|
|
72
|
+
first_seen?: number;
|
|
73
|
+
last_seen: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BouncerSignalRow {
|
|
77
|
+
id: number;
|
|
78
|
+
device?: BouncerDeviceRow | number | null;
|
|
79
|
+
muid: string;
|
|
80
|
+
duid?: string;
|
|
81
|
+
msid: string;
|
|
82
|
+
mtab?: string;
|
|
83
|
+
session_id?: string;
|
|
84
|
+
stage: BouncerStage;
|
|
85
|
+
ip_address: string | null;
|
|
86
|
+
page_type: string;
|
|
87
|
+
raw_signals?: Record<string, unknown>;
|
|
88
|
+
server_signals?: Record<string, unknown>;
|
|
89
|
+
risk_score: number;
|
|
90
|
+
decision: BouncerDecision;
|
|
91
|
+
triggered_signals?: string[];
|
|
92
|
+
geo_ip?: BouncerGeoIp | null;
|
|
93
|
+
created: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface BotSignatureRow {
|
|
97
|
+
id: number;
|
|
98
|
+
sig_type: BotSignatureType;
|
|
99
|
+
value: string;
|
|
100
|
+
source: BotSignatureSource;
|
|
101
|
+
confidence: number;
|
|
102
|
+
hit_count: number;
|
|
103
|
+
block_count?: number;
|
|
104
|
+
expires_at: number | null;
|
|
105
|
+
is_active: boolean;
|
|
106
|
+
notes?: string;
|
|
107
|
+
created?: number;
|
|
108
|
+
modified: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface BouncerIncidentRow {
|
|
112
|
+
id: number;
|
|
113
|
+
created: number;
|
|
114
|
+
priority: number;
|
|
115
|
+
state: string;
|
|
116
|
+
status: string;
|
|
117
|
+
scope: string;
|
|
118
|
+
category: string;
|
|
119
|
+
country_code: string | null;
|
|
120
|
+
title: string | null;
|
|
121
|
+
details: string | null;
|
|
122
|
+
model_name: string | null;
|
|
123
|
+
model_id: number | null;
|
|
124
|
+
source_ip: string | null;
|
|
125
|
+
hostname: string | null;
|
|
126
|
+
metadata: Record<string, unknown>;
|
|
127
|
+
group_id: number | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Strip a sensitive backend-only field before any list/detail/save cache write. */
|
|
131
|
+
export function sanitizeBouncerSignalRow(row: BouncerSignalRow): BouncerSignalRow {
|
|
132
|
+
const safe = { ...row } as BouncerSignalRow & Record<string, unknown>;
|
|
133
|
+
delete safe.token_nonce;
|
|
134
|
+
return safe;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function listGraph(params: Params): Params {
|
|
138
|
+
const safe = { ...params };
|
|
139
|
+
delete safe.graph;
|
|
140
|
+
return { ...safe, graph: 'list' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const BouncerSignalModel = defineModel<BouncerSignalRow>({
|
|
144
|
+
name: 'bouncer_signal',
|
|
145
|
+
endpoint: '/api/account/bouncer/signal',
|
|
146
|
+
permissions: { view: BOUNCER_VIEW_PERMS },
|
|
147
|
+
normalizeListParams: listGraph,
|
|
148
|
+
sanitizeRow: sanitizeBouncerSignalRow,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const BouncerDeviceModel = defineModel<BouncerDeviceRow>({
|
|
152
|
+
name: 'bouncer_device',
|
|
153
|
+
endpoint: '/api/account/bouncer/device',
|
|
154
|
+
permissions: { view: BOUNCER_VIEW_PERMS, manage: BOUNCER_MANAGE_PERMS },
|
|
155
|
+
normalizeListParams: listGraph,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const BotSignatureModel = defineModel<BotSignatureRow>({
|
|
159
|
+
name: 'bot_signature',
|
|
160
|
+
endpoint: '/api/account/bouncer/signature',
|
|
161
|
+
permissions: { view: BOUNCER_VIEW_PERMS, manage: BOUNCER_MANAGE_PERMS },
|
|
162
|
+
normalizeListParams: listGraph,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const BouncerIncidentModel = defineModel<BouncerIncidentRow>({
|
|
166
|
+
name: 'bouncer_incident',
|
|
167
|
+
endpoint: '/api/incident/incident',
|
|
168
|
+
permissions: { view: BOUNCER_INCIDENT_VIEW_PERMS },
|
|
169
|
+
sanitizeRow: sanitizeIncidentRow,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/** Graph is part of the key so a sparse default record cannot satisfy detail. */
|
|
173
|
+
export function bouncerSignalDetailKey(id: number) {
|
|
174
|
+
return [BouncerSignalModel.endpoint, 'one', id, { graph: 'detail' }] as const;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function useBouncerSignalDetail(id: number | null) {
|
|
178
|
+
return useQuery({
|
|
179
|
+
queryKey: id == null
|
|
180
|
+
? [BouncerSignalModel.endpoint, 'one', null, { graph: 'detail' }] as const
|
|
181
|
+
: bouncerSignalDetailKey(id),
|
|
182
|
+
queryFn: async () => {
|
|
183
|
+
const body = await mojoCall(`${BouncerSignalModel.endpoint}/${id!}`, {
|
|
184
|
+
params: { graph: 'detail' },
|
|
185
|
+
});
|
|
186
|
+
return sanitizeBouncerSignalRow(body.data as BouncerSignalRow);
|
|
187
|
+
},
|
|
188
|
+
enabled: id != null,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const BOUNCER_SIGNATURE_TYPES: ReadonlyArray<{ value: BotSignatureType; label: string }> = [
|
|
193
|
+
{ value: 'ip', label: 'IP Address' },
|
|
194
|
+
{ value: 'subnet_24', label: 'Subnet /24' },
|
|
195
|
+
{ value: 'subnet_16', label: 'Subnet /16' },
|
|
196
|
+
{ value: 'user_agent', label: 'User Agent' },
|
|
197
|
+
{ value: 'fingerprint', label: 'Browser Fingerprint' },
|
|
198
|
+
{ value: 'signal_set', label: 'Signal Set (Campaign)' },
|
|
199
|
+
];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
Badge, DetailView, Eyebrow, FlatRow, JsonBlock, ModelTable,
|
|
4
|
+
fmt, groupByDay, modal,
|
|
5
|
+
type Column, type FilterDef, type Tone,
|
|
6
|
+
} from '../../ui';
|
|
7
|
+
import {
|
|
8
|
+
BouncerSignalModel, bouncerSignalDetailKey, useBouncerSignalDetail,
|
|
9
|
+
type BouncerDecision, type BouncerSignalRow,
|
|
10
|
+
} from './models';
|
|
11
|
+
|
|
12
|
+
export function bouncerDecisionTone(decision: BouncerDecision): Tone {
|
|
13
|
+
if (decision === 'allow') return 'success';
|
|
14
|
+
if (decision === 'monitor') return 'warning';
|
|
15
|
+
if (decision === 'block') return 'danger';
|
|
16
|
+
if (decision === 'log') return 'info';
|
|
17
|
+
return 'muted';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function bouncerRiskTone(score: number): Tone {
|
|
21
|
+
if (score >= 80) return 'danger';
|
|
22
|
+
if (score >= 50) return 'warning';
|
|
23
|
+
if (score >= 20) return 'info';
|
|
24
|
+
return 'success';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function EmbeddedDevice({ signal }: { signal: BouncerSignalRow }) {
|
|
28
|
+
const device = signal.device && typeof signal.device === 'object' ? signal.device : null;
|
|
29
|
+
if (!device) return <p className="dim-italic">No device record was linked to this assessment.</p>;
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<Eyebrow>Linked device</Eyebrow>
|
|
33
|
+
<FlatRow label="MUID"><code>{device.muid}</code></FlatRow>
|
|
34
|
+
<FlatRow label="DUID"><code>{device.duid || '—'}</code></FlatRow>
|
|
35
|
+
<FlatRow label="Fingerprint"><code>{device.fingerprint_id || '—'}</code></FlatRow>
|
|
36
|
+
<FlatRow label="Risk tier"><Badge tone={device.risk_tier === 'low' ? 'success' : device.risk_tier === 'medium' ? 'warning' : device.risk_tier === 'unknown' ? 'muted' : 'danger'}>{device.risk_tier}</Badge></FlatRow>
|
|
37
|
+
<FlatRow label="Activity">{device.event_count} events · {device.block_count} blocks</FlatRow>
|
|
38
|
+
<FlatRow label="Last seen">{fmt.datetime(device.last_seen)}</FlatRow>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function GeoIp({ signal }: { signal: BouncerSignalRow }) {
|
|
44
|
+
const geo = signal.geo_ip;
|
|
45
|
+
if (!geo) return <p className="dim-italic">No GeoIP record was linked to this assessment.</p>;
|
|
46
|
+
const flags = [
|
|
47
|
+
geo.is_tor && 'Tor', geo.is_vpn && 'VPN', geo.is_proxy && 'Proxy',
|
|
48
|
+
geo.is_cloud && 'Cloud', geo.is_datacenter && 'Datacenter',
|
|
49
|
+
geo.is_known_attacker && 'Known attacker', geo.is_known_abuser && 'Known abuser',
|
|
50
|
+
].filter((flag): flag is string => Boolean(flag));
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<Eyebrow>GeoIP</Eyebrow>
|
|
54
|
+
<FlatRow label="Location">{[geo.city, geo.region, geo.country_name || geo.country_code].filter(Boolean).join(', ') || '—'}</FlatRow>
|
|
55
|
+
<FlatRow label="Network">{geo.asn_org || geo.isp || geo.asn || '—'}</FlatRow>
|
|
56
|
+
<FlatRow label="Threat level"><Badge>{geo.threat_level || 'unknown'}</Badge></FlatRow>
|
|
57
|
+
<FlatRow label="Signals">
|
|
58
|
+
<span className="bouncer-token-list">
|
|
59
|
+
{flags.length ? flags.map((flag) => <Badge key={flag} tone="warning">{flag}</Badge>) : <span className="dim">None</span>}
|
|
60
|
+
</span>
|
|
61
|
+
</FlatRow>
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function BouncerSignalDetail({ id, onClose }: { id: number; onClose: () => void }) {
|
|
67
|
+
const queryClient = useQueryClient();
|
|
68
|
+
const { data: signal, isPending, error } = useBouncerSignalDetail(id);
|
|
69
|
+
if (isPending) return <div className="modal-pad dim">Loading signal assessment…</div>;
|
|
70
|
+
if (!signal || error) return <div className="modal-pad text-bad">{error?.message ?? 'Signal assessment not found'}</div>;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<DetailView
|
|
74
|
+
icon="bi-activity"
|
|
75
|
+
title="Signal Assessment"
|
|
76
|
+
subtitle={`${signal.ip_address || 'Unknown IP'} · ${fmt.datetime(signal.created)}`}
|
|
77
|
+
chips={[
|
|
78
|
+
{ text: signal.decision.toUpperCase(), tone: bouncerDecisionTone(signal.decision) },
|
|
79
|
+
{ text: `Risk ${signal.risk_score}`, tone: bouncerRiskTone(signal.risk_score) },
|
|
80
|
+
{ text: signal.stage, tone: 'muted' },
|
|
81
|
+
]}
|
|
82
|
+
sections={[
|
|
83
|
+
{
|
|
84
|
+
key: 'overview', label: 'Overview', icon: 'bi-grid-1x2', render: () => (
|
|
85
|
+
<>
|
|
86
|
+
<Eyebrow>Assessment</Eyebrow>
|
|
87
|
+
<FlatRow label="Decision"><Badge tone={bouncerDecisionTone(signal.decision)}>{signal.decision.toUpperCase()}</Badge></FlatRow>
|
|
88
|
+
<FlatRow label="Risk score"><strong className={`bouncer-risk bouncer-risk-${bouncerRiskTone(signal.risk_score)}`}>{signal.risk_score}</strong></FlatRow>
|
|
89
|
+
<FlatRow label="Stage">{signal.stage}</FlatRow>
|
|
90
|
+
<FlatRow label="Page type">{signal.page_type || '—'}</FlatRow>
|
|
91
|
+
<FlatRow label="IP address"><code>{signal.ip_address || '—'}</code></FlatRow>
|
|
92
|
+
<FlatRow label="Created">{fmt.datetime(signal.created)}</FlatRow>
|
|
93
|
+
<Eyebrow>Identity</Eyebrow>
|
|
94
|
+
<FlatRow label="MUID"><code>{signal.muid || '—'}</code></FlatRow>
|
|
95
|
+
<FlatRow label="DUID"><code>{signal.duid || '—'}</code></FlatRow>
|
|
96
|
+
<FlatRow label="MSID"><code>{signal.msid || '—'}</code></FlatRow>
|
|
97
|
+
<FlatRow label="MTAB"><code>{signal.mtab || '—'}</code></FlatRow>
|
|
98
|
+
<FlatRow label="Session"><code>{signal.session_id || '—'}</code></FlatRow>
|
|
99
|
+
<Eyebrow>Triggered signals</Eyebrow>
|
|
100
|
+
<div className="bouncer-token-list">
|
|
101
|
+
{signal.triggered_signals?.length
|
|
102
|
+
? signal.triggered_signals.map((name) => <Badge key={name} tone="warning">{name}</Badge>)
|
|
103
|
+
: <span className="dim-italic">No signals triggered.</span>}
|
|
104
|
+
</div>
|
|
105
|
+
</>
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
{ key: 'device', label: 'Device', icon: 'bi-fingerprint', render: () => <EmbeddedDevice signal={signal} /> },
|
|
109
|
+
{ key: 'geoip', label: 'GeoIP', icon: 'bi-geo-alt', render: () => <GeoIp signal={signal} /> },
|
|
110
|
+
{ key: 'raw', label: 'Raw Signals', icon: 'bi-code-square', render: () => <JsonBlock label="Client signal payload" value={signal.raw_signals ?? {}} defaultOpen /> },
|
|
111
|
+
{ key: 'server', label: 'Server Signals', icon: 'bi-hdd-stack', render: () => <JsonBlock label="Server signal payload" value={signal.server_signals ?? {}} defaultOpen /> },
|
|
112
|
+
]}
|
|
113
|
+
contextMenu={[
|
|
114
|
+
{
|
|
115
|
+
label: 'Refresh', icon: 'bi-arrow-clockwise', onSelect: () => {
|
|
116
|
+
void queryClient.invalidateQueries({ queryKey: bouncerSignalDetailKey(signal.id) });
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
]}
|
|
120
|
+
onClose={onClose}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function showBouncerSignalDetail(id: number): void {
|
|
126
|
+
void modal.detail((close) => (
|
|
127
|
+
<BouncerSignalDetail id={id} onClose={() => close(null)} />
|
|
128
|
+
));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const SIGNAL_COLUMNS: Column<BouncerSignalRow>[] = [
|
|
132
|
+
{ key: 'created', label: 'Timestamp', sortable: true, hideable: false, render: (row) => fmt.datetime(row.created) },
|
|
133
|
+
{ key: 'ip_address', label: 'IP', render: (row) => <code>{row.ip_address || '—'}</code> },
|
|
134
|
+
{ key: 'decision', label: 'Decision', render: (row) => <Badge tone={bouncerDecisionTone(row.decision)}>{row.decision.toUpperCase()}</Badge> },
|
|
135
|
+
{ key: 'risk_score', label: 'Risk', sortable: true, align: 'end', render: (row) => <strong className={`bouncer-risk bouncer-risk-${bouncerRiskTone(row.risk_score)}`}>{row.risk_score}</strong> },
|
|
136
|
+
{ key: 'page_type', label: 'Page', render: (row) => row.page_type || '—' },
|
|
137
|
+
{ key: 'stage', label: 'Stage', render: (row) => <Badge tone="muted">{row.stage}</Badge> },
|
|
138
|
+
{ key: 'muid', label: 'Device', render: (row) => <code title={row.muid}>{fmt.truncateMiddle(row.muid, 16)}</code> },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const SIGNAL_FILTERS: FilterDef[] = [
|
|
142
|
+
{ key: 'created', label: 'Created', type: 'daterange' },
|
|
143
|
+
{ key: 'ip_address', label: 'IP address', type: 'text' },
|
|
144
|
+
{ key: 'decision', label: 'Decision', type: 'select', options: [
|
|
145
|
+
{ value: 'allow', label: 'Allow' }, { value: 'monitor', label: 'Monitor' },
|
|
146
|
+
{ value: 'block', label: 'Block' }, { value: 'log', label: 'Log' },
|
|
147
|
+
] },
|
|
148
|
+
{ key: 'risk_score', label: 'Minimum risk', type: 'number', lookup: 'gte' },
|
|
149
|
+
{ key: 'page_type', label: 'Page type', type: 'text' },
|
|
150
|
+
{ key: 'stage', label: 'Stage', type: 'select', options: [
|
|
151
|
+
{ value: 'assess', label: 'Assess' }, { value: 'submit', label: 'Submit' }, { value: 'event', label: 'Event' },
|
|
152
|
+
] },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const signalGroups = groupByDay<BouncerSignalRow>('created');
|
|
156
|
+
|
|
157
|
+
export function BouncerSignalsPage() {
|
|
158
|
+
return (
|
|
159
|
+
<ModelTable<BouncerSignalRow>
|
|
160
|
+
model={BouncerSignalModel}
|
|
161
|
+
eyebrow="Security · Bouncer"
|
|
162
|
+
title="Signal Decisions"
|
|
163
|
+
searchPlaceholder="Search MUID, DUID, IP, or decision"
|
|
164
|
+
columns={SIGNAL_COLUMNS}
|
|
165
|
+
filters={SIGNAL_FILTERS}
|
|
166
|
+
presets={[
|
|
167
|
+
{ key: 'all', label: 'All', params: {} },
|
|
168
|
+
{ key: 'blocked', label: 'Blocked', params: { decision: 'block' } },
|
|
169
|
+
{ key: 'monitor', label: 'Monitor', params: { decision: 'monitor' } },
|
|
170
|
+
]}
|
|
171
|
+
defaultSort="-created"
|
|
172
|
+
columnChooser
|
|
173
|
+
persistState
|
|
174
|
+
persistKey="admin:bouncer:signals"
|
|
175
|
+
exportFormats={['csv', 'json']}
|
|
176
|
+
onRowClick={(row) => showBouncerSignalDetail(row.id)}
|
|
177
|
+
groupBy={signalGroups.groupBy}
|
|
178
|
+
groupHeaderLabel={signalGroups.groupHeaderLabel}
|
|
179
|
+
groupHeaderStyle="mark"
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
Badge, DetailView, Eyebrow, FlatRow, ModelTable, SchemaForm,
|
|
4
|
+
fmt, modal, toast,
|
|
5
|
+
type BatchAction, type Column, type Field, type FilterDef, type FormData,
|
|
6
|
+
} from '../../ui';
|
|
7
|
+
import { useCan } from '../../client';
|
|
8
|
+
import {
|
|
9
|
+
BOUNCER_MANAGE_PERMS, BOUNCER_SIGNATURE_TYPES, BotSignatureModel,
|
|
10
|
+
type BotSignatureRow, type BotSignatureType,
|
|
11
|
+
} from './models';
|
|
12
|
+
|
|
13
|
+
const SIGNATURE_FIELDS: Field[] = [
|
|
14
|
+
{
|
|
15
|
+
name: 'sig_type', type: 'select', label: 'Signature type', required: true, columns: 6,
|
|
16
|
+
options: BOUNCER_SIGNATURE_TYPES.map((option) => ({ ...option })),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'value', type: 'text', label: 'Value', required: true, columns: 6,
|
|
20
|
+
help: 'The exact value or pattern evaluated by this signature type.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'confidence', type: 'text', label: 'Confidence (0–100)', required: true, columns: 6,
|
|
24
|
+
placeholder: '80',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'expires_at', type: 'datetimepicker', label: 'Expires', columns: 6,
|
|
28
|
+
help: 'Optional for manual signatures; leave empty for no expiry.',
|
|
29
|
+
},
|
|
30
|
+
{ name: 'notes', type: 'textarea', label: 'Notes' },
|
|
31
|
+
{ name: 'is_active', type: 'switch', label: 'Active' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function signatureType(value: unknown): BotSignatureType {
|
|
35
|
+
const candidate = String(value ?? '');
|
|
36
|
+
const exact = BOUNCER_SIGNATURE_TYPES.find((option) => option.value === candidate);
|
|
37
|
+
if (!exact) throw new Error('Choose a valid signature type');
|
|
38
|
+
return exact.value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function optionalEpoch(value: unknown): number | null {
|
|
42
|
+
if (value == null || value === '') return null;
|
|
43
|
+
const epoch = Number(value);
|
|
44
|
+
if (!Number.isFinite(epoch) || epoch <= 0) throw new Error('Choose a valid expiry');
|
|
45
|
+
return epoch;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function signatureChanges(data: FormData, creating: boolean): Record<string, unknown> {
|
|
49
|
+
const value = String(data.value ?? '').trim();
|
|
50
|
+
if (!value) throw new Error('Value is required');
|
|
51
|
+
const confidence = Number(String(data.confidence ?? '').trim());
|
|
52
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 100) {
|
|
53
|
+
throw new Error('Confidence must be a number from 0 to 100');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
sig_type: signatureType(data.sig_type),
|
|
57
|
+
value,
|
|
58
|
+
confidence,
|
|
59
|
+
expires_at: optionalEpoch(data.expires_at),
|
|
60
|
+
notes: String(data.notes ?? ''),
|
|
61
|
+
is_active: data.is_active === true,
|
|
62
|
+
...(creating ? { source: 'manual' as const } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function SignatureEditor({ row, close }: {
|
|
67
|
+
row?: BotSignatureRow;
|
|
68
|
+
close: (saved: boolean) => void;
|
|
69
|
+
}) {
|
|
70
|
+
const save = BotSignatureModel.useSave();
|
|
71
|
+
const creating = row == null;
|
|
72
|
+
const initial: FormData = creating
|
|
73
|
+
? { sig_type: 'ip', confidence: 80, expires_at: null, notes: '', is_active: true }
|
|
74
|
+
: {
|
|
75
|
+
sig_type: row.sig_type,
|
|
76
|
+
value: row.value,
|
|
77
|
+
confidence: row.confidence,
|
|
78
|
+
expires_at: row.expires_at,
|
|
79
|
+
notes: row.notes ?? '',
|
|
80
|
+
is_active: row.is_active,
|
|
81
|
+
};
|
|
82
|
+
return (
|
|
83
|
+
<div className="modal-pad">
|
|
84
|
+
<h2 className="modal-title">{creating ? 'Create manual bot signature' : 'Edit bot signature'}</h2>
|
|
85
|
+
{!creating && (
|
|
86
|
+
<p className="dim">
|
|
87
|
+
Source <Badge tone={row.source === 'auto' ? 'info' : 'primary'}>{row.source.toUpperCase()}</Badge>
|
|
88
|
+
{' '}· hits {row.hit_count} · blocks {row.block_count ?? 0}
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
<SchemaForm
|
|
92
|
+
fields={SIGNATURE_FIELDS}
|
|
93
|
+
initial={initial}
|
|
94
|
+
submitText={creating ? 'Create signature' : 'Save changes'}
|
|
95
|
+
onCancel={() => close(false)}
|
|
96
|
+
onSubmit={async (data) => {
|
|
97
|
+
await save.mutateAsync({
|
|
98
|
+
id: row?.id ?? null,
|
|
99
|
+
changes: signatureChanges(data, creating),
|
|
100
|
+
});
|
|
101
|
+
toast.success(creating ? 'Bot signature created' : 'Bot signature updated');
|
|
102
|
+
close(true);
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function openSignatureEditor(row?: BotSignatureRow): Promise<void> {
|
|
110
|
+
await modal.open<boolean>((close) => <SignatureEditor row={row} close={close} />, { size: 'lg' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function SignatureDetail({ row, onClose }: { row: BotSignatureRow; onClose: () => void }) {
|
|
114
|
+
return (
|
|
115
|
+
<DetailView
|
|
116
|
+
icon="bi-shield-check"
|
|
117
|
+
title={row.value}
|
|
118
|
+
subtitle={`Bot signature #${row.id}`}
|
|
119
|
+
chips={[
|
|
120
|
+
{ text: row.sig_type, tone: 'info' },
|
|
121
|
+
{ text: row.source.toUpperCase(), tone: row.source === 'auto' ? 'info' : 'primary' },
|
|
122
|
+
{ text: row.is_active ? 'ACTIVE' : 'INACTIVE', tone: row.is_active ? 'success' : 'muted' },
|
|
123
|
+
]}
|
|
124
|
+
sections={[
|
|
125
|
+
{
|
|
126
|
+
key: 'overview', label: 'Overview', icon: 'bi-grid-1x2', render: () => (
|
|
127
|
+
<>
|
|
128
|
+
<Eyebrow>Signature</Eyebrow>
|
|
129
|
+
<FlatRow label="Type">{row.sig_type}</FlatRow>
|
|
130
|
+
<FlatRow label="Value"><code>{row.value}</code></FlatRow>
|
|
131
|
+
<FlatRow label="Source">{row.source}</FlatRow>
|
|
132
|
+
<FlatRow label="Confidence">{row.confidence}%</FlatRow>
|
|
133
|
+
<FlatRow label="Hits">{row.hit_count}</FlatRow>
|
|
134
|
+
<FlatRow label="Blocks">{row.block_count ?? 0}</FlatRow>
|
|
135
|
+
<FlatRow label="Expires">{row.expires_at == null ? 'Never' : fmt.datetime(row.expires_at)}</FlatRow>
|
|
136
|
+
<FlatRow label="Modified">{fmt.datetime(row.modified)}</FlatRow>
|
|
137
|
+
<Eyebrow>Notes</Eyebrow>
|
|
138
|
+
<p className={row.notes ? undefined : 'dim-italic'}>{row.notes || 'No notes.'}</p>
|
|
139
|
+
</>
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
]}
|
|
143
|
+
onClose={onClose}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const SIGNATURE_COLUMNS: Column<BotSignatureRow>[] = [
|
|
149
|
+
{ key: 'sig_type', label: 'Type', sortable: true, render: (row) => <Badge tone="info">{row.sig_type}</Badge> },
|
|
150
|
+
{ key: 'value', label: 'Value', hideable: false, render: (row) => <code title={row.value}>{fmt.truncate(row.value, 60)}</code> },
|
|
151
|
+
{ key: 'source', label: 'Source', render: (row) => <Badge tone={row.source === 'auto' ? 'info' : 'primary'}>{row.source.toUpperCase()}</Badge> },
|
|
152
|
+
{ key: 'confidence', label: 'Confidence', sortable: true, align: 'end', render: (row) => `${row.confidence}%` },
|
|
153
|
+
{ key: 'hit_count', label: 'Hits', sortable: true, align: 'end' },
|
|
154
|
+
{ key: 'is_active', label: 'Status', render: (row) => <Badge tone={row.is_active ? 'success' : 'muted'}>{row.is_active ? 'ON' : 'OFF'}</Badge> },
|
|
155
|
+
{ key: 'expires_at', label: 'Expires', sortable: true, render: (row) => row.expires_at == null ? 'Never' : fmt.datetime(row.expires_at) },
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const SIGNATURE_FILTERS: FilterDef[] = [
|
|
159
|
+
{ key: 'sig_type', label: 'Type', type: 'select', options: BOUNCER_SIGNATURE_TYPES.map((option) => ({ ...option })) },
|
|
160
|
+
{ key: 'value', label: 'Value', type: 'text' },
|
|
161
|
+
{ key: 'source', label: 'Source', type: 'select', options: [
|
|
162
|
+
{ value: 'auto', label: 'Auto' }, { value: 'manual', label: 'Manual' },
|
|
163
|
+
] },
|
|
164
|
+
{ key: 'is_active', label: 'Status', type: 'boolean', trueLabel: 'Active', falseLabel: 'Inactive' },
|
|
165
|
+
{ key: 'expires_at', label: 'Expires', type: 'daterange' },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
export function BotSignaturesPage() {
|
|
169
|
+
const queryClient = useQueryClient();
|
|
170
|
+
const save = BotSignatureModel.useSave();
|
|
171
|
+
const { can: canManage } = useCan(BOUNCER_MANAGE_PERMS);
|
|
172
|
+
|
|
173
|
+
const batchActions: BatchAction<BotSignatureRow>[] = canManage ? [
|
|
174
|
+
{
|
|
175
|
+
key: 'enable', label: 'Enable', icon: 'bi-check-circle',
|
|
176
|
+
run: (row) => save.mutateAsync({ id: row.id, changes: { is_active: true } }),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: 'disable', label: 'Disable', icon: 'bi-pause-circle',
|
|
180
|
+
run: (row) => save.mutateAsync({ id: row.id, changes: { is_active: false } }),
|
|
181
|
+
},
|
|
182
|
+
] : [];
|
|
183
|
+
|
|
184
|
+
const openRow = async (row: BotSignatureRow) => {
|
|
185
|
+
try {
|
|
186
|
+
const detailed = await BotSignatureModel.fetchOne(queryClient, row.id);
|
|
187
|
+
if (canManage) await openSignatureEditor(detailed);
|
|
188
|
+
else await modal.detail((close) => <SignatureDetail row={detailed} onClose={() => close(null)} />);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
toast.error(error instanceof Error ? error.message : 'Failed to load bot signature');
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<ModelTable<BotSignatureRow>
|
|
196
|
+
model={BotSignatureModel}
|
|
197
|
+
eyebrow="Security · Bouncer"
|
|
198
|
+
title="Bot Signatures"
|
|
199
|
+
searchPlaceholder="Search signature type, value, or source"
|
|
200
|
+
columns={SIGNATURE_COLUMNS}
|
|
201
|
+
filters={SIGNATURE_FILTERS}
|
|
202
|
+
presets={[
|
|
203
|
+
{ key: 'all', label: 'All', params: {} },
|
|
204
|
+
{ key: 'active', label: 'Active', params: { is_active: 'true' } },
|
|
205
|
+
{ key: 'manual', label: 'Manual', params: { source: 'manual' } },
|
|
206
|
+
{ key: 'auto', label: 'Auto', params: { source: 'auto' } },
|
|
207
|
+
]}
|
|
208
|
+
defaultSort="-modified"
|
|
209
|
+
columnChooser
|
|
210
|
+
persistState
|
|
211
|
+
persistKey="admin:bouncer:signatures"
|
|
212
|
+
exportFormats={['csv', 'json']}
|
|
213
|
+
selectable={canManage}
|
|
214
|
+
batchActions={batchActions}
|
|
215
|
+
addLabel="New Signature"
|
|
216
|
+
onAdd={canManage ? () => void openSignatureEditor() : undefined}
|
|
217
|
+
onRowClick={(row) => void openRow(row)}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { MetricsChart } from '../../charts';
|
|
3
|
+
import { useAuthSnapshot } from '../../client';
|
|
4
|
+
import { fmt } from '../../ui';
|
|
5
|
+
import {
|
|
6
|
+
CLOUDWATCH_GRANULARITIES, loadCloudWatchSeries,
|
|
7
|
+
type CloudWatchChartDefinition, type CloudWatchUnit,
|
|
8
|
+
} from './data';
|
|
9
|
+
|
|
10
|
+
function valueFormatter(unit: CloudWatchUnit): (value: number) => string {
|
|
11
|
+
if (unit === 'percent') return (value) => `${value.toLocaleString()}%`;
|
|
12
|
+
if (unit === 'bytes') return (value) => fmt.filesize(value);
|
|
13
|
+
if (unit === 'seconds') return (value) => `${value.toLocaleString()} s`;
|
|
14
|
+
return (value) => value.toLocaleString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CloudWatchChart({ definition, slugs = [], refreshSignal }: {
|
|
18
|
+
definition: CloudWatchChartDefinition;
|
|
19
|
+
slugs?: string[];
|
|
20
|
+
refreshSignal?: unknown;
|
|
21
|
+
}) {
|
|
22
|
+
const auth = useAuthSnapshot();
|
|
23
|
+
return <MetricsChart
|
|
24
|
+
title={definition.title}
|
|
25
|
+
account={definition.account}
|
|
26
|
+
slugs={slugs}
|
|
27
|
+
apiParams={{ category: definition.category, stat: 'avg' }}
|
|
28
|
+
loadSeries={loadCloudWatchSeries}
|
|
29
|
+
seriesCacheKey={`admin-cloudwatch:${auth.uid ?? 'anonymous'}:${definition.account}:${definition.category}`}
|
|
30
|
+
preserveSeriesLabels
|
|
31
|
+
allowedGranularities={[...CLOUDWATCH_GRANULARITIES]}
|
|
32
|
+
refreshSignal={refreshSignal}
|
|
33
|
+
defaultRange="24h"
|
|
34
|
+
defaultGranularity="hours"
|
|
35
|
+
height={200}
|
|
36
|
+
valueFormatter={valueFormatter(definition.unit)}
|
|
37
|
+
showRefresh={false}
|
|
38
|
+
/>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function LazyCloudWatchChart({ definition, slugs, refreshSignal, eager = false }: {
|
|
42
|
+
definition: CloudWatchChartDefinition;
|
|
43
|
+
slugs?: string[];
|
|
44
|
+
refreshSignal?: unknown;
|
|
45
|
+
eager?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
48
|
+
const [mounted, setMounted] = useState(eager);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (mounted || !ref.current) return;
|
|
51
|
+
if (typeof IntersectionObserver === 'undefined') { setMounted(true); return; }
|
|
52
|
+
const observer = new IntersectionObserver((entries) => {
|
|
53
|
+
if (entries.some((entry) => entry.isIntersecting)) { setMounted(true); observer.disconnect(); }
|
|
54
|
+
}, { rootMargin: '240px' });
|
|
55
|
+
observer.observe(ref.current);
|
|
56
|
+
return () => observer.disconnect();
|
|
57
|
+
}, [mounted]);
|
|
58
|
+
return <div ref={ref} className="cloudwatch-chart-slot">
|
|
59
|
+
{mounted ? <CloudWatchChart definition={definition} slugs={slugs} refreshSignal={refreshSignal} /> : <div className="panel panel-pad cloudwatch-chart-placeholder"><span className="skel skel-block" /></div>}
|
|
60
|
+
</div>;
|
|
61
|
+
}
|