diff --git a/apps/api/src/cloud-security/cloud-security.service.ts b/apps/api/src/cloud-security/cloud-security.service.ts index af8cba373..f249e0c57 100644 --- a/apps/api/src/cloud-security/cloud-security.service.ts +++ b/apps/api/src/cloud-security/cloud-security.service.ts @@ -387,6 +387,7 @@ export class CloudSecurityService { ...variables, detectedServices: [...existingDetected], disabledServices: [...updatedDisabled], + serviceDetectionCompletedAt: new Date().toISOString(), }, }, }); diff --git a/apps/api/src/integration-platform/controllers/services.controller.ts b/apps/api/src/integration-platform/controllers/services.controller.ts index 427aba61b..5ed883bea 100644 --- a/apps/api/src/integration-platform/controllers/services.controller.ts +++ b/apps/api/src/integration-platform/controllers/services.controller.ts @@ -63,6 +63,19 @@ export class ServicesController { ? (variables.enabledServices as string[]) : null; + const source = legacyEnabledServices + ? 'legacy-enabled' + : detectedServices + ? 'detected' + : 'manifest-default'; + const detectionCompletedAt = + typeof variables.serviceDetectionCompletedAt === 'string' + ? variables.serviceDetectionCompletedAt + : null; + const detectionReady = providerSlug === 'gcp' + ? source !== 'manifest-default' || Boolean(detectionCompletedAt) + : true; + // AWS security baseline: always scanned, hidden from Services tab const BASELINE_SERVICES = providerSlug === 'aws' ? new Set(['cloudtrail', 'config', 'guardduty', 'iam', 'cloudwatch', 'kms']) @@ -89,15 +102,8 @@ export class ServicesController { } else if (detectedServices) { enabled = detectedServices.includes(s.id) && !disabledServices.has(s.id); } else { - // No detection data yet: - // - GCP should default to enabled (scan already runs across SCC categories by default), - // so UI doesn't misleadingly show everything as OFF immediately after OAuth connect. - // - Others keep manifest defaults. - if (providerSlug === 'gcp') { - enabled = !disabledServices.has(s.id); - } else { - enabled = (s.enabledByDefault ?? true) && !disabledServices.has(s.id); - } + // Default: use enabledByDefault from manifest, otherwise enabled + enabled = (s.enabledByDefault ?? true) && !disabledServices.has(s.id); } return { @@ -108,6 +114,12 @@ export class ServicesController { enabled, }; }), + meta: { + providerSlug, + source, + detectionReady, + detectionCompletedAt, + }, }; } } diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx index baead1038..518b97c64 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx @@ -199,6 +199,7 @@ export function CloudTestsSection({ description?: string; } | null>(null); const [showSetupDialog, setShowSetupDialog] = useState(false); + const [showGcpSetupStatus, setShowGcpSetupStatus] = useState(false); const findingsResponse = api.useSWR<{ data: Finding[]; count: number }>( '/v1/cloud-security/findings', @@ -464,6 +465,15 @@ export function CloudTestsSection({

+ {providerSlug === 'gcp' && ( + + )}
); } diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ProviderTabs.tsx b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ProviderTabs.tsx index 50898c9a7..a8bdac085 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ProviderTabs.tsx +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ProviderTabs.tsx @@ -88,7 +88,12 @@ function CloudConnectionContent({ onScanComplete: () => void; }) { const api = useApi(); - const { services, refresh: refreshServices, updateServices } = useConnectionServices(connection.id); + const { + services, + meta: servicesMeta, + refresh: refreshServices, + updateServices, + } = useConnectionServices(connection.id); const [togglingService, setTogglingService] = useState(null); const detectedRef = useRef(false); @@ -130,6 +135,8 @@ function CloudConnectionContent({ })); const enabledCount = services.filter((s) => s.enabled).length; + const waitingForDetection = connection.integrationId === 'gcp' && servicesMeta.detectionReady === false; + const showEnabledCount = !waitingForDetection; return ( @@ -138,7 +145,7 @@ function CloudConnectionContent({ Activity Remediations - Services{enabledCount > 0 ? ` (${enabledCount})` : ''} + Services{showEnabledCount && enabledCount > 0 ? ` (${enabledCount})` : ''} @@ -188,7 +195,14 @@ function CloudConnectionContent({ - {manifestServices.length > 0 ? ( + {waitingForDetection ? ( +
+

Detecting active GCP services...

+

+ We'll show real service toggles as soon as detection completes. +

+
+ ) : manifestServices.length > 0 ? ( (null); + const waitingForDetection = meta.providerSlug === 'gcp' && meta.detectionReady === false; const filteredServices = useMemo(() => { if (!search) return services.filter((s) => s.implemented !== false); @@ -76,7 +77,11 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps

Daily scan

-

Every day at 5:00 AM UTC

+

+ {waitingForDetection + ? 'Detecting active GCP services...' + : 'Every day at 5:00 AM UTC'} +

Active @@ -87,12 +92,14 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps

- {enabledCount} of {implementedServices.length} services + {waitingForDetection + ? 'Waiting for detection' + : `${enabledCount} of ${implementedServices.length} services`}

{/* Search (only if many services) */} - {implementedServices.length > 8 && ( + {implementedServices.length > 8 && !waitingForDetection && (
@@ -118,7 +125,12 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps {/* Service list */}
- {filteredServices.map((service) => ( + {waitingForDetection ? ( +

+ Service toggles will appear once detection completes. +

+ ) : ( + filteredServices.map((service) => ( - ))} - {filteredServices.length === 0 && search && ( + )) + )} + {!waitingForDetection && filteredServices.length === 0 && search && (

No services match "{search}"

diff --git a/apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/ProviderDetailView.tsx b/apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/ProviderDetailView.tsx index d8a56b6c3..45d852390 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/ProviderDetailView.tsx +++ b/apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/ProviderDetailView.tsx @@ -84,6 +84,7 @@ export function ProviderDetailView({ provider, initialConnections }: ProviderDet // Services hook for the selected connection const { services: connectionServices, + meta: servicesMeta, refresh: refreshServices, updateServices, } = useConnectionServices(selectedConnection?.id ?? null); @@ -251,13 +252,22 @@ export function ProviderDetailView({ provider, initialConnections }: ProviderDet {services.length > 0 && (

Services

- + {provider.id === 'gcp' && servicesMeta.detectionReady === false ? ( +
+

Detecting active GCP services...

+

+ We'll show real service toggles as soon as detection completes. +

+
+ ) : ( + + )}
)}
diff --git a/apps/app/src/hooks/use-integration-platform.ts b/apps/app/src/hooks/use-integration-platform.ts index c7c43ac59..c6eb9c80d 100644 --- a/apps/app/src/hooks/use-integration-platform.ts +++ b/apps/app/src/hooks/use-integration-platform.ts @@ -595,13 +595,23 @@ interface ConnectionServiceItem { enabled: boolean; } +interface ConnectionServicesMeta { + providerSlug?: string; + source?: 'legacy-enabled' | 'detected' | 'manifest-default'; + detectionReady?: boolean; + detectionCompletedAt?: string | null; +} + +interface ConnectionServicesResponse { + services: ConnectionServiceItem[]; + meta?: ConnectionServicesMeta; +} + export function useConnectionServices(connectionId: string | null) { - const { data, error, isLoading, mutate } = useSWR<{ - services: ConnectionServiceItem[]; - }>( + const { data, error, isLoading, mutate } = useSWR( connectionId ? ['connection-services', connectionId] : null, async () => { - const response = await api.get<{ services: ConnectionServiceItem[] }>( + const response = await api.get( `/v1/integrations/connections/${connectionId}/services`, ); if (response.error) { @@ -613,6 +623,10 @@ export function useConnectionServices(connectionId: string | null) { ); const services = useMemo(() => data?.services ?? [], [data]); + const meta = useMemo( + () => data?.meta ?? { detectionReady: true }, + [data], + ); const updateServices = useCallback( async (serviceId: string, enabled: boolean) => { @@ -638,10 +652,10 @@ export function useConnectionServices(connectionId: string | null) { return { services, + meta, isLoading, error: error?.message, refresh: mutate, updateServices, }; } -