From dd6ed6e8917d4fcc6f1528f648f98f2c271325cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:02:28 -0400 Subject: [PATCH 1/2] ENG-39 Create new job on trigger for a full policy regen (#1701) * feat(app): add option to regenerate all policies * fix(app): rename 'Regenerate full policies' to 'Regenerate all policies' * fix(app): rename description on policy regen dialog by removing 'mark it for review' --------- Co-authored-by: chasprowebdev --- .../[policyId]/components/PolicyOverview.tsx | 2 +- .../all/actions/regenerate-full-policies.ts | 30 +++++++ .../components/FullPolicyHeaderActions.tsx | 82 +++++++++++++++++++ .../app/(app)/[orgId]/policies/all/page.tsx | 6 +- .../onboarding/generate-full-policies.ts | 52 ++++++++++++ 5 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/policies/all/actions/regenerate-full-policies.ts create mode 100644 apps/app/src/app/(app)/[orgId]/policies/all/components/FullPolicyHeaderActions.tsx create mode 100644 apps/app/src/jobs/tasks/onboarding/generate-full-policies.ts diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyOverview.tsx b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyOverview.tsx index 2c20745e9..f8fb688b0 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyOverview.tsx @@ -231,7 +231,7 @@ export function PolicyOverview({ toast.info('Regeneration started'); }} title="Regenerate Policy" - description="This will regenerate the policy content and mark it for review. Continue?" + description="This will regenerate the policy content. Continue?" confirmText="Regenerate" confirmIcon={} /> diff --git a/apps/app/src/app/(app)/[orgId]/policies/all/actions/regenerate-full-policies.ts b/apps/app/src/app/(app)/[orgId]/policies/all/actions/regenerate-full-policies.ts new file mode 100644 index 000000000..bf50ef88b --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/policies/all/actions/regenerate-full-policies.ts @@ -0,0 +1,30 @@ +'use server'; + +import { authActionClient } from '@/actions/safe-action'; +import { generateFullPolicies } from '@/jobs/tasks/onboarding/generate-full-policies'; +import { tasks } from '@trigger.dev/sdk'; +import { z } from 'zod'; + +export const regenerateFullPoliciesAction = authActionClient + .inputSchema(z.object({})) + .metadata({ + name: 'regenerate-full-policies', + track: { + event: 'regenerate-full-policies', + channel: 'server', + }, + }) + .action(async ({ ctx }) => { + const { session } = ctx; + + if (!session?.activeOrganizationId) { + throw new Error('No active organization'); + } + + await tasks.trigger('generate-full-policies', { + organizationId: session.activeOrganizationId, + }); + + // Revalidation handled by safe-action middleware using x-pathname header + return { success: true }; + }); diff --git a/apps/app/src/app/(app)/[orgId]/policies/all/components/FullPolicyHeaderActions.tsx b/apps/app/src/app/(app)/[orgId]/policies/all/components/FullPolicyHeaderActions.tsx new file mode 100644 index 000000000..231b186ba --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/policies/all/components/FullPolicyHeaderActions.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { Button } from '@comp/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@comp/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@comp/ui/dropdown-menu'; +import { Icons } from '@comp/ui/icons'; +import { useAction } from 'next-safe-action/hooks'; +import { useState } from 'react'; +import { toast } from 'sonner'; +import { regenerateFullPoliciesAction } from '../actions/regenerate-full-policies'; + +export function FullPolicyHeaderActions() { + const [isRegenerateConfirmOpen, setRegenerateConfirmOpen] = useState(false); + + const regenerate = useAction(regenerateFullPoliciesAction, { + onSuccess: () => { + toast.success('Policy regeneration started. This may take a few minutes.'); + setRegenerateConfirmOpen(false); + }, + onError: (error) => { + toast.error(error.error.serverError || 'Failed to regenerate policies'); + }, + }); + + const handleRegenerate = async () => { + await regenerate.execute({}); + }; + + return ( + <> + + + + + + setRegenerateConfirmOpen(true)}> + Regenerate all policies + + + + + {/* Regenerate Confirmation Dialog */} + + + + Regenerate All Policies + + This will generate new policy content for all policies using your org context and + frameworks. Continue? + + + + + + + + + + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/policies/all/page.tsx b/apps/app/src/app/(app)/[orgId]/policies/all/page.tsx index c36a56805..af42d76d0 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/all/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/all/page.tsx @@ -2,6 +2,7 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { getValidFilters } from '@/lib/data-table'; import type { SearchParams } from '@/types'; import type { Metadata } from 'next'; +import { FullPolicyHeaderActions } from './components/FullPolicyHeaderActions'; import { PoliciesTable } from './components/policies-table'; import { getPolicies } from './data/queries'; import { searchParamsCache } from './data/validations'; @@ -23,7 +24,10 @@ export default async function PoliciesPage({ ...props }: PolicyTableProps) { ]); return ( - + } + > ); diff --git a/apps/app/src/jobs/tasks/onboarding/generate-full-policies.ts b/apps/app/src/jobs/tasks/onboarding/generate-full-policies.ts new file mode 100644 index 000000000..04caad31b --- /dev/null +++ b/apps/app/src/jobs/tasks/onboarding/generate-full-policies.ts @@ -0,0 +1,52 @@ +import { db } from '@db'; +import { logger, queue, task } from '@trigger.dev/sdk'; +import { getOrganizationContext, triggerPolicyUpdates } from './onboard-organization-helpers'; + +// v4 queues must be declared in advance +const generateFullPoliciesQueue = queue({ + name: 'generate-full-policies', + concurrencyLimit: 100, +}); + +export const generateFullPolicies = task({ + id: 'generate-full-policies', + queue: generateFullPoliciesQueue, + retry: { + maxAttempts: 3, + }, + run: async (payload: { organizationId: string }) => { + logger.info(`Starting full policy generation for organization ${payload.organizationId}`); + + try { + // Get organization context + const { questionsAndAnswers } = await getOrganizationContext(payload.organizationId); + + // Get frameworks + const frameworkInstances = await db.frameworkInstance.findMany({ + where: { + organizationId: payload.organizationId, + }, + }); + + const frameworks = await db.frameworkEditorFramework.findMany({ + where: { + id: { + in: frameworkInstances.map((instance) => instance.frameworkId), + }, + }, + }); + + // Trigger policy updates for all policies + await triggerPolicyUpdates(payload.organizationId, questionsAndAnswers, frameworks); + + logger.info( + `Successfully triggered policy updates for organization ${payload.organizationId}`, + ); + } catch (error) { + logger.error(`Error during policy generation for organization ${payload.organizationId}:`, { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, +}); From 9abfc4a2bd0b320d1e804f9b3cf4a714f52d1002 Mon Sep 17 00:00:00 2001 From: Alex Alaniz <88956822+Alex-Alaniz@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:11:59 -0400 Subject: [PATCH 2/2] fix(cloud-tests): improve error messages and user feedback (#1703) * fix(cloud-tests): improve error messages and user feedback - Extract clean messages from GCP/Azure error responses - Add info banner explaining propagation delays (12-24h) for empty results - Auto-dismiss error banners after 30 seconds Fixes JSON blob display in GCP errors, adds context for empty scan results, and prevents error banner clutter. * fix(cloud-tests): update propagation delay timing to 24-48 hours Updated propagation delay messaging from 12-24 hours to 24-48 hours to better reflect real-world cloud security service propagation times. Also removed "or run another scan" text as rescanning doesn't affect propagation delays. --------- Co-authored-by: Mariano Fuentes --- .../cloud-tests/components/ResultsView.tsx | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ResultsView.tsx b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ResultsView.tsx index bea6ce0fd..d7a68a281 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ResultsView.tsx +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ResultsView.tsx @@ -3,7 +3,7 @@ import { Button } from '@comp/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; import { useRealtimeRun } from '@trigger.dev/react-hooks'; -import { CheckCircle2, Loader2, RefreshCw, X } from 'lucide-react'; +import { CheckCircle2, Info, Loader2, RefreshCw, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { FindingsTable } from './FindingsTable'; @@ -27,6 +27,23 @@ interface ResultsViewProps { const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; +// Helper function to extract clean error messages from cloud provider errors +function extractCleanErrorMessage(errorMessage: string): string { + try { + // Try to parse as JSON (GCP returns JSON blob) + const parsed = JSON.parse(errorMessage); + + // GCP error structure: { error: { message: "actual message" } } + if (parsed.error?.message) { + return parsed.error.message; + } + } catch { + // Not JSON, return original + } + + return errorMessage; +} + export function ResultsView({ findings, scanTaskId, @@ -46,6 +63,7 @@ export function ResultsView({ const [selectedStatus, setSelectedStatus] = useState('all'); const [selectedSeverity, setSelectedSeverity] = useState('all'); const [showSuccessBanner, setShowSuccessBanner] = useState(false); + const [showErrorBanner, setShowErrorBanner] = useState(false); // Show success banner when scan completes, auto-hide after 5 seconds useEffect(() => { @@ -58,6 +76,17 @@ export function ResultsView({ } }, [scanCompleted]); + // Auto-dismiss error banner after 30 seconds + useEffect(() => { + if (scanFailed) { + setShowErrorBanner(true); + const timer = setTimeout(() => { + setShowErrorBanner(false); + }, 30000); + return () => clearTimeout(timer); + } + }, [scanFailed]); + // Get unique statuses and severities const uniqueStatuses = Array.from( new Set(findings.map((f) => f.status).filter(Boolean) as string[]), @@ -117,15 +146,36 @@ export function ResultsView({ )} - {scanFailed && !isScanning && ( + {/* Propagation delay info banner - only when scan succeeds but returns empty output */} + {scanCompleted && findings.length === 0 && !isScanning && !scanFailed && ( +
+ +
+

Initial scan complete

+

+ Security findings may take 24-48 hours to appear after enabling cloud security services. Check back later. +

+
+
+ )} + + {showErrorBanner && scanFailed && !isScanning && (

Scan failed

- {run?.error?.message || 'An error occurred during the scan. Please try again.'} + {extractCleanErrorMessage(run?.error?.message || 'An error occurred during the scan. Please try again.')}

+
)}