Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/src/cloud-security/cloud-security.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ export class CloudSecurityService {
...variables,
detectedServices: [...existingDetected],
disabledServices: [...updatedDisabled],
serviceDetectionCompletedAt: new Date().toISOString(),
},
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand All @@ -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 {
Expand All @@ -108,6 +114,12 @@ export class ServicesController {
enabled,
};
}),
meta: {
providerSlug,
source,
detectionReady,
detectionCompletedAt,
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -464,6 +465,15 @@ export function CloudTestsSection({
</p>
</div>
<div className="flex items-center gap-2">
{providerSlug === 'gcp' && (
<Button
variant="outline"
size="sm"
onClick={() => setShowGcpSetupStatus(true)}
>
Setup status
</Button>
)}
<ScheduledScanPopover connectionId={connectionId} />
<Button
variant="outline"
Expand Down Expand Up @@ -915,6 +925,27 @@ export function CloudTestsSection({
loadCapabilities();
}}
/>

{providerSlug === 'gcp' && (
<Dialog open={showGcpSetupStatus} onOpenChange={setShowGcpSetupStatus}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>GCP setup status</DialogTitle>
<DialogDescription>
Verify API and IAM readiness for reliable Security Command Center scans.
</DialogDescription>
</DialogHeader>
<div className="max-h-[70vh] overflow-y-auto pr-1">
<GcpSetupGuide
connectionId={connectionId}
hasOrgId={Boolean(variables?.organization_id)}
onRunScan={handleRunScan}
isScanning={isScanning}
/>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const detectedRef = useRef(false);

Expand Down Expand Up @@ -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 (
<Tabs defaultValue="findings">
Expand All @@ -138,7 +145,7 @@ function CloudConnectionContent({
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="remediations">Remediations</TabsTrigger>
<TabsTrigger value="services">
Services{enabledCount > 0 ? ` (${enabledCount})` : ''}
Services{showEnabledCount && enabledCount > 0 ? ` (${enabledCount})` : ''}
</TabsTrigger>
</TabsList>

Expand Down Expand Up @@ -188,7 +195,14 @@ function CloudConnectionContent({
</span>
</div>
</div>
{manifestServices.length > 0 ? (
{waitingForDetection ? (
<div className="rounded-lg border bg-muted/20 px-4 py-3">
<p className="text-sm font-medium">Detecting active GCP services...</p>
<p className="text-xs text-muted-foreground mt-0.5">
We&apos;ll show real service toggles as soon as detection completes.
</p>
</div>
) : manifestServices.length > 0 ? (
<ServicesGrid
services={manifestServices}
connectionServices={services}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ interface ScheduledScanPopoverProps {

export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps) {
const apiClient = useApi();
const { services, refresh: refreshServices } = useConnectionServices(connectionId);
const { services, meta, refresh: refreshServices } = useConnectionServices(connectionId);
const [search, setSearch] = useState('');
const [saving, setSaving] = useState<string | null>(null);
const waitingForDetection = meta.providerSlug === 'gcp' && meta.detectionReady === false;

const filteredServices = useMemo(() => {
if (!search) return services.filter((s) => s.implemented !== false);
Expand Down Expand Up @@ -76,7 +77,11 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps
<div className="flex items-center justify-between border-b px-4 py-3">
<div>
<p className="text-sm font-medium">Daily scan</p>
<p className="text-xs text-muted-foreground">Every day at 5:00 AM UTC</p>
<p className="text-xs text-muted-foreground">
{waitingForDetection
? 'Detecting active GCP services...'
: 'Every day at 5:00 AM UTC'}
</p>
</div>
<span className="inline-flex items-center rounded-full bg-primary/10 border border-primary/20 px-2 py-0.5 text-[10px] font-medium text-primary">
Active
Expand All @@ -87,12 +92,14 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps
<div className="px-4 pt-3 pb-1">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">
{enabledCount} of {implementedServices.length} services
{waitingForDetection
? 'Waiting for detection'
: `${enabledCount} of ${implementedServices.length} services`}
</p>
<button
type="button"
onClick={handleEnableAll}
disabled={saving === 'all' || enabledCount === implementedServices.length}
disabled={waitingForDetection || saving === 'all' || enabledCount === implementedServices.length}
className="text-[11px] font-medium text-primary hover:text-primary/80 disabled:text-muted-foreground transition-colors"
>
Enable all
Expand All @@ -101,7 +108,7 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps
</div>

{/* Search (only if many services) */}
{implementedServices.length > 8 && (
{implementedServices.length > 8 && !waitingForDetection && (
<div className="px-4 py-1.5">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground/50" />
Expand All @@ -118,7 +125,12 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps

{/* Service list */}
<div className="max-h-[280px] overflow-y-auto px-2 py-1">
{filteredServices.map((service) => (
{waitingForDetection ? (
<p className="py-3 text-center text-xs text-muted-foreground">
Service toggles will appear once detection completes.
</p>
) : (
filteredServices.map((service) => (
<label
key={service.id}
className={cn(
Expand All @@ -134,8 +146,9 @@ export function ScheduledScanPopover({ connectionId }: ScheduledScanPopoverProps
/>
<span className="truncate">{service.name}</span>
</label>
))}
{filteredServices.length === 0 && search && (
))
)}
{!waitingForDetection && filteredServices.length === 0 && search && (
<p className="py-3 text-center text-xs text-muted-foreground">
No services match &quot;{search}&quot;
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -251,13 +252,22 @@ export function ProviderDetailView({ provider, initialConnections }: ProviderDet
{services.length > 0 && (
<div>
<h3 className="text-sm font-semibold mb-3">Services</h3>
<ServicesGrid
services={services}
connectionServices={connectionServices}
connectionId={selectedConnection?.id ?? null}
onToggle={handleToggleService}
togglingService={togglingService}
/>
{provider.id === 'gcp' && servicesMeta.detectionReady === false ? (
<div className="rounded-lg border bg-muted/20 px-4 py-3">
<p className="text-sm font-medium">Detecting active GCP services...</p>
<p className="text-xs text-muted-foreground mt-0.5">
We&apos;ll show real service toggles as soon as detection completes.
</p>
</div>
) : (
<ServicesGrid
services={services}
connectionServices={connectionServices}
connectionId={selectedConnection?.id ?? null}
onToggle={handleToggleService}
togglingService={togglingService}
/>
)}
</div>
)}
</div>
Expand Down
24 changes: 19 additions & 5 deletions apps/app/src/hooks/use-integration-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectionServicesResponse>(
connectionId ? ['connection-services', connectionId] : null,
async () => {
const response = await api.get<{ services: ConnectionServiceItem[] }>(
const response = await api.get<ConnectionServicesResponse>(
`/v1/integrations/connections/${connectionId}/services`,
);
if (response.error) {
Expand All @@ -613,6 +623,10 @@ export function useConnectionServices(connectionId: string | null) {
);

const services = useMemo(() => data?.services ?? [], [data]);
const meta = useMemo<ConnectionServicesMeta>(
() => data?.meta ?? { detectionReady: true },
[data],
);

const updateServices = useCallback(
async (serviceId: string, enabled: boolean) => {
Expand All @@ -638,10 +652,10 @@ export function useConnectionServices(connectionId: string | null) {

return {
services,
meta,
isLoading,
error: error?.message,
refresh: mutate,
updateServices,
};
}

Loading