Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
665f454
feat(integrations): add cloud services as evidence integrations (GCP/…
tofikwest Jun 1, 2026
e84d747
Merge branch 'main' into worktree-cloud-posture-task-mappings
tofikwest Jun 1, 2026
5f6bebc
fix(integrations): address cubic review — fix 30 verified cloud-check…
tofikwest Jun 2, 2026
9df1803
Merge remote-tracking branch 'origin/worktree-cloud-posture-task-mapp…
tofikwest Jun 2, 2026
d1c6368
fix(integrations): address cubic 2nd-pass review (10 findings)
tofikwest Jun 2, 2026
220982b
fix(integrations): cubic 3rd-pass — scan continuity, Aurora backups, …
tofikwest Jun 2, 2026
a467ff9
fix(integration-platform): address cubic round-4 review (27 findings)
tofikwest Jun 2, 2026
6ef7ad0
fix(integration-platform): extend round-4 patterns to sibling check f…
tofikwest Jun 2, 2026
0b8a52d
Merge branch 'main' into worktree-cloud-posture-task-mappings
tofikwest Jun 2, 2026
3ec918a
fix(integration-platform): resolve 3 P1s from cubic review of fix com…
tofikwest Jun 2, 2026
46c7ff6
Merge remote-tracking branch 'origin/worktree-cloud-posture-task-mapp…
tofikwest Jun 2, 2026
239aea4
fix(integration-platform): resolve cubic findings on latest commit (4…
tofikwest Jun 2, 2026
96e1f04
Merge branch 'main' into worktree-cloud-posture-task-mappings
tofikwest Jun 2, 2026
67a23d8
Merge branch 'main' into worktree-cloud-posture-task-mappings
tofikwest Jun 2, 2026
b1b5579
fix(integration-platform): resolve 4 cubic findings (RBAC gate + read…
tofikwest Jun 2, 2026
78e987c
Merge remote-tracking branch 'origin/worktree-cloud-posture-task-mapp…
tofikwest Jun 2, 2026
d59e4c7
fix(integration-platform): never let a read failure end as a silent/f…
tofikwest Jun 2, 2026
4a6b64a
fix(integration-platform): address cubic review of d59e4c7ca (2 P2s)
tofikwest Jun 2, 2026
63dab6d
Merge branch 'main' into worktree-cloud-posture-task-mappings
tofikwest Jun 2, 2026
cdde662
fix(integration-platform): close GCP IAM silent pass, harden CloudTra…
tofikwest Jun 2, 2026
05d1cce
fix(integration-platform): include out-of-scope role defs in azure rb…
tofikwest Jun 3, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,34 @@ export class ConnectionsController {
description: s.description,
enabledByDefault: s.enabledByDefault ?? true,
implemented: s.implemented ?? true,
mappedTasks: this.buildServiceTaskMappings(m.checks, s.id),
})) ?? [],
};
});
}

/**
* Evidence tasks a single service's checks satisfy: distinct taskMappings of
* the manifest checks whose `service` equals serviceId, resolved to names.
*/
private buildServiceTaskMappings(
checks:
| ReadonlyArray<{ service?: string; taskMapping?: TaskTemplateId }>
| undefined,
serviceId: string,
): Array<{ id: string; name: string }> {
const out: Array<{ id: string; name: string }> = [];
const seen = new Set<string>();
for (const check of checks ?? []) {
if (check.service !== serviceId || !check.taskMapping) continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per-service task mapping now silently excludes checks without a service tag, causing empty/incorrect evidence-task counts for cloud providers like Vercel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/integration-platform/controllers/connections.controller.ts, line 327:

<comment>Per-service task mapping now silently excludes checks without a `service` tag, causing empty/incorrect evidence-task counts for cloud providers like Vercel.</comment>

<file context>
@@ -305,11 +305,34 @@ export class ConnectionsController {
+    const out: Array<{ id: string; name: string }> = [];
+    const seen = new Set<string>();
+    for (const check of checks ?? []) {
+      if (check.service !== serviceId || !check.taskMapping) continue;
+      if (seen.has(check.taskMapping)) continue;
+      seen.add(check.taskMapping);
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per-service task mapping now silently excludes checks without a service tag, causing empty/incorrect evidence-task counts for cloud providers like Vercel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/integration-platform/controllers/connections.controller.ts, line 327:

<comment>Per-service task mapping now silently excludes checks without a `service` tag, causing empty/incorrect evidence-task counts for cloud providers like Vercel.</comment>

<file context>
@@ -305,11 +305,34 @@ export class ConnectionsController {
+    const out: Array<{ id: string; name: string }> = [];
+    const seen = new Set<string>();
+    for (const check of checks ?? []) {
+      if (check.service !== serviceId || !check.taskMapping) continue;
+      if (seen.has(check.taskMapping)) continue;
+      seen.add(check.taskMapping);
</file context>

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per-service task mapping now silently excludes checks without a service tag, causing empty/incorrect evidence-task counts for cloud providers like Vercel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/integration-platform/controllers/connections.controller.ts, line 327:

<comment>Per-service task mapping now silently excludes checks without a `service` tag, causing empty/incorrect evidence-task counts for cloud providers like Vercel.</comment>

<file context>
@@ -305,11 +305,34 @@ export class ConnectionsController {
+    const out: Array<{ id: string; name: string }> = [];
+    const seen = new Set<string>();
+    for (const check of checks ?? []) {
+      if (check.service !== serviceId || !check.taskMapping) continue;
+      if (seen.has(check.taskMapping)) continue;
+      seen.add(check.taskMapping);
</file context>
Fix with Cubic

if (seen.has(check.taskMapping)) continue;
seen.add(check.taskMapping);
const info = TASK_TEMPLATE_INFO[check.taskMapping];
if (info) out.push({ id: check.taskMapping, name: info.name });
}
return out;
}

/**
* Get a specific provider's details
*/
Expand Down Expand Up @@ -398,6 +421,7 @@ export class ConnectionsController {
description: s.description,
enabledByDefault: s.enabledByDefault ?? true,
implemented: s.implemented ?? true,
mappedTasks: this.buildServiceTaskMappings(manifest.checks, s.id),
})) ?? [],
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';

import { Button } from '@trycompai/design-system';
import { ArrowRight } from '@trycompai/design-system/icons';
import Link from 'next/link';

/** The resolved live task for a mapped template, when it exists in the org. */
export interface EvidenceTaskRowTask {
taskId: string;
name: string;
description: string;
}

interface EvidenceTaskRowProps {
/** Name shown when the template has no live task in this org. */
fallbackName: string;
task?: EvidenceTaskRowTask;
orgId: string;
/** Action label when the task exists (e.g. 'Open', 'View task'). */
buttonLabel?: string;
/** When the tasks fetch failed, distinguish "couldn't load" from "not added". */
tasksErrored?: boolean;
}

/**
* A single evidence-task row: task name + description with an "open" action, or
* a not-added / load-error fallback. Shared by the integration detail page and
* the per-service detail page so the row markup has one source of truth.
*/
export function EvidenceTaskRow({
fallbackName,
task,
orgId,
buttonLabel = 'Open',
tasksErrored = false,
}: EvidenceTaskRowProps) {
return (
<div className="flex items-center justify-between gap-4 px-4 py-3">
<div className="min-w-0">
<p className="truncate text-sm font-medium">{task?.name ?? fallbackName}</p>
<p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
{task?.description ||
'Mapped to this template, but the task is not in this organization yet.'}
</p>
</div>

{task ? (
<Button
size="sm"
variant="outline"
render={<Link href={`/${orgId}/tasks/${task.taskId}`} />}
iconRight={<ArrowRight size={14} />}
>
{buttonLabel}
</Button>
) : (
<span className="shrink-0 text-xs text-muted-foreground">
{tasksErrored ? 'Couldn’t load tasks' : 'Not added'}
</span>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
'use client';

import type { IntegrationProvider } from '@/hooks/use-integration-platform';
import { Button } from '@trycompai/design-system';
import { ArrowRight } from '@trycompai/design-system/icons';
import Link from 'next/link';
import { useMemo } from 'react';
import { EvidenceTaskRow } from './EvidenceTaskRow';

export interface IntegrationTaskTemplate {
id: string;
Expand Down Expand Up @@ -56,34 +54,14 @@ export function IntegrationEvidenceTasks({
</div>

<div className="divide-y">
{mappedTasks.map((mappedTask) => {
const task = taskByTemplateId.get(mappedTask.id);

return (
<div key={mappedTask.id} className="flex items-center justify-between gap-4 px-4 py-3">
<div className="min-w-0">
<p className="truncate text-sm font-medium">{task?.name ?? mappedTask.name}</p>
<p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
{task?.description ||
'This task is mapped to the integration template, but is not available in this organization yet.'}
</p>
</div>

{task ? (
<Button
size="sm"
variant="outline"
render={<Link href={`/${orgId}/tasks/${task.taskId}`} />}
iconRight={<ArrowRight size={14} />}
>
Open
</Button>
) : (
<span className="shrink-0 text-xs text-muted-foreground">Not added</span>
)}
</div>
);
})}
{mappedTasks.map((mappedTask) => (
<EvidenceTaskRow
key={mappedTask.id}
fallbackName={mappedTask.name}
task={taskByTemplateId.get(mappedTask.id)}
orgId={orgId}
/>
))}
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function ProviderDetailView({
name: string;
description: string;
implemented?: boolean;
mappedTasks?: Array<{ id: string; name: string }>;
}>;
}
).services ?? [],
Expand All @@ -97,9 +98,7 @@ export function ProviderDetailView({
services: connectionServices,
meta: servicesMeta,
refresh: refreshServices,
updateServices,
} = useConnectionServices(selectedConnection?.id ?? null);
const [togglingService, setTogglingService] = useState<string | null>(null);
const [gcpOrgs, setGcpOrgs] = useState<
Array<{
id: string;
Expand All @@ -111,25 +110,6 @@ export function ProviderDetailView({
const oauthBootstrapHandledRef = useRef(false);
const settingsQueryHandledRef = useRef(false);

const handleToggleService = useCallback(
async (serviceId: string, enabled: boolean): Promise<boolean> => {
setTogglingService(serviceId);
try {
await updateServices(serviceId, enabled);
toast.success(
`${services.find((s) => s.id === serviceId)?.name ?? serviceId} ${enabled ? 'enabled' : 'disabled'}`,
);
return true;
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to update');
return false;
} finally {
setTogglingService(null);
}
},
[updateServices, services],
);

// OAuth return (?success=true): strip query, detect org/projects (NOT services yet — user must select projects first)
useEffect(() => {
if (
Expand Down Expand Up @@ -415,8 +395,8 @@ export function ProviderDetailView({
services={services}
connectionServices={connectionServices}
connectionId={selectedConnection?.id ?? null}
onToggle={handleToggleService}
togglingService={togglingService}
orgId={orgId}
slug={provider.id}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client';

import { useConnectionServices } from '@/hooks/use-integration-platform';
import { ChevronRight } from '@trycompai/design-system/icons';
import { Badge } from '@trycompai/ui/badge';
import Link from 'next/link';
import {
Cloud,
Database,
Expand Down Expand Up @@ -72,6 +74,7 @@ interface ServiceMeta {
description: string;
enabledByDefault?: boolean;
implemented?: boolean;
mappedTasks?: Array<{ id: string; name: string }>;
}

function ServiceIcon({ serviceId }: { serviceId: string }) {
Expand All @@ -87,80 +90,84 @@ function ServiceIcon({ serviceId }: { serviceId: string }) {
interface ServiceCardProps {
service: ServiceMeta;
connectionId: string | null;
isConnected: boolean;
onToggle?: (id: string, enabled: boolean) => void | Promise<boolean | void>;
toggling?: boolean;
orgId: string;
slug: string;
}

export function ServiceCard({
service,
connectionId,
isConnected,
onToggle,
toggling,
}: ServiceCardProps) {
const { services } = useConnectionServices(connectionId);

/**
* A service row inside a cloud integration's detail page. Clicking navigates to
* the per-service detail page (where the Cloud Tests scan toggle + the evidence
* tasks it satisfies live). The row itself shows current scan status + the
* count of evidence tasks the service maps to — it is NOT a toggle.
*/
export function ServiceCard({ service, connectionId, orgId, slug }: ServiceCardProps) {
const { services, isLoading, error } = useConnectionServices(connectionId);
const isImplemented = service.implemented !== false;
const liveService = services.find((s) => s.id === service.id);
const inServiceList = Boolean(liveService);
const isEnabled = liveService?.enabled ?? false;
const showToggle = isImplemented && isConnected && onToggle;
// Don't assert a scan status until the connection's live services have
// loaded. A service absent from the loaded list (e.g. AWS baseline services)
// is always scanned — but only treat "absent" as "always scanned" once the
// fetch has actually succeeded.
const servicesLoaded = Boolean(connectionId) && !isLoading && !error;
let scanningOn = false;
let scanningLabel: string;
if (!servicesLoaded) {
scanningLabel = error ? 'Status unavailable' : 'Checking status…';
} else if (!inServiceList) {
scanningOn = true;
scanningLabel = 'Always scanned';
} else {
scanningOn = isEnabled;
scanningLabel = isEnabled ? 'Scanning on' : 'Scanning off';
}
const taskCount = service.mappedTasks?.length ?? 0;

const href =
`/${encodeURIComponent(orgId)}/integrations/${encodeURIComponent(slug)}/services/${encodeURIComponent(service.id)}` +
(connectionId ? `?connectionId=${encodeURIComponent(connectionId)}` : '');

return (
<div
className={`relative rounded-lg border p-4 ${
!isImplemented
? 'opacity-50'
: isEnabled && isConnected
? 'border-primary/30 bg-primary/5 dark:border-primary/20 dark:bg-primary/5'
: ''
<Link
href={href}
className={`group relative flex items-start gap-3 rounded-lg border p-4 transition-colors hover:border-primary/40 hover:bg-muted/40 ${
!isImplemented ? 'opacity-50' : ''
}`}
>
<div className="flex items-start gap-3 min-w-0">
<ServiceIcon serviceId={service.id} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{service.name}</span>
{!isImplemented && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
Coming Soon
</Badge>
)}
</div>
<p className="text-muted-foreground mt-0.5 text-xs leading-relaxed">
{service.description}
</p>
{liveService?.projects && liveService.projects.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
{liveService.projects.map((pid) => (
<span
key={pid}
className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground"
>
{pid}
</span>
))}
</div>
<ServiceIcon serviceId={service.id} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{service.name}</span>
{!isImplemented && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
Coming Soon
</Badge>
)}
</div>
{showToggle && (
<button
role="switch"
aria-checked={isEnabled}
disabled={toggling}
onClick={() => void Promise.resolve(onToggle?.(service.id, !isEnabled)).catch(() => {})}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full transition-colors disabled:opacity-50 ${
isEnabled ? 'bg-primary' : 'bg-muted-foreground/30'
}`}
>
<p className="text-muted-foreground mt-0.5 line-clamp-2 text-xs leading-relaxed">
{service.description}
</p>
<div className="mt-2 flex items-center gap-3 text-[11px] text-muted-foreground">
<span className="inline-flex items-center gap-1">
<span
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-transform ${
isEnabled ? 'translate-x-[18px]' : 'translate-x-[3px]'
className={`h-1.5 w-1.5 rounded-full ${
scanningOn ? 'bg-primary' : 'bg-muted-foreground/40'
}`}
/>
</button>
)}
{scanningLabel}
</span>
{taskCount > 0 && (
<span>
{taskCount} evidence task{taskCount === 1 ? '' : 's'}
</span>
)}
</div>
</div>
</div>
<ChevronRight
size={16}
className="mt-0.5 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5"
/>
</Link>
);
}
Loading
Loading