diff --git a/src/Shuffle-Core/api.ts b/src/Shuffle-Core/api.ts index 0cc8b79bf..ef4103606 100644 --- a/src/Shuffle-Core/api.ts +++ b/src/Shuffle-Core/api.ts @@ -22,15 +22,10 @@ const PROD_BACKEND = 'https://shuffler.io'; const CLOUD_DOMAINS = ['security.shuffler.io', 'shutdown.no']; const getEnvVar = (key: string): string | undefined => { - // Indirect access via `new Function` keeps `import.meta` out of the emitted - // CJS bundle. tsup otherwise inlines it verbatim into `dist/index.js`, which - // breaks consumers whose webpack rolls the CJS build into a non-ESM bundle - // ("Cannot use 'import.meta' outside a module"). + // Dynamic property access avoids tsup's static inlining of import.meta.env.* + // while still working in Vite's dev server and build output. try { - const meta = (new Function('try { return import.meta } catch { return undefined }')()) as - | { env?: Record } - | undefined; - return meta?.env?.[key]; + return (import.meta as any).env?.[key]; } catch { return undefined; } diff --git a/src/Shuffle-Core/config/usecases.ts b/src/Shuffle-Core/config/usecases.ts index f9902d49c..6e3cc88ba 100644 --- a/src/Shuffle-Core/config/usecases.ts +++ b/src/Shuffle-Core/config/usecases.ts @@ -323,7 +323,7 @@ export interface Usecase { /** Optional custom action — a one-click CTA that overrides the default "create workflow" flow. * Use for usecases best fulfilled by an in-app navigation (e.g. opening /monitors?add_host=true) * rather than generating a Shuffle workflow. */ - customAction?: { + customAction?: null | { /** Button label shown on the usecase detail page (e.g. "Add Monitor"). */ label: string; /** In-app route. Use this OR `url`. Internal routes use react-router navigation. */ @@ -578,17 +578,16 @@ export const DEFAULT_USECASES: Usecase[] = [ }, }, { - id: 'asset_management_case_management_vuln_1', phase: 'ingest', source: 'asset_management', target: 'case_management', + id: 'asset_management_case_management_vuln_1', phase: 'correlation', source: 'asset_management', target: 'case_management', label: 'Vulnerability Correlation', + animated: true, tags: ['Context', 'Correlation', 'Vulnerability'], description: 'Correlate known vulnerabilities (CVEs, misconfigurations, missing patches) on affected assets with active incidents — surfacing exploitable weaknesses that elevate risk and guide containment priorities.', agenticDescription: 'An agent matches observables and affected hosts in a case against the vulnerability inventory, identifies exploitable CVEs aligned with the attack technique, recalculates incident severity, and recommends remediation or compensating controls.', + automationLabel: 'Vulnerability Correlation', + automationCategory: 'cases', automationArea: 'correlation', - customAction: { - label: 'Configure Vulnerabilities', - href: '/vulnerabilities', - description: 'Open the vulnerability inventory to ingest CVEs from your scanners.', - }, + customAction: null, }, { id: 'vulnerability_ingestion_1', phase: 'ingest', source: 'asset_management', target: 'case_management', diff --git a/src/Shuffle-Core/views/Usecases.tsx b/src/Shuffle-Core/views/Usecases.tsx index e88353b4b..649b526a1 100644 --- a/src/Shuffle-Core/views/Usecases.tsx +++ b/src/Shuffle-Core/views/Usecases.tsx @@ -350,7 +350,7 @@ export interface Usecase { /** Optional custom action — a one-click CTA that overrides the default "create workflow" flow. * Use for usecases best fulfilled by an in-app navigation (e.g. opening /monitors?add_host=true) * rather than generating a Shuffle workflow. */ - customAction?: { + customAction?: null | { /** Button label shown on the usecase detail page (e.g. "Add Monitor"). */ label: string; /** In-app route. Use this OR `url`. Internal routes use react-router navigation. */ @@ -485,6 +485,7 @@ const DESTINATION_SHUFFLE_ONLY_FLOW_IDS = new Set([ 'threat_intel_case_management_1', // Enrichment ]); + // ── Per-usecase "I just picked this app" persistence ────────────────────────── // When the user picks an app from the AppSearchDrawer we both wire it into the // workflow (best-effort) AND record it in localStorage so the next time they @@ -674,17 +675,16 @@ export const DEFAULT_USECASES: Usecase[] = [ }, }, { - id: 'asset_management_case_management_vuln_1', phase: 'ingest', source: 'asset_management', target: 'case_management', + id: 'asset_management_case_management_vuln_1', phase: 'correlation', source: 'asset_management', target: 'case_management', label: 'Vulnerability Correlation', + animated: true, tags: ['Context', 'Correlation', 'Vulnerability'], description: 'Correlate known vulnerabilities (CVEs, misconfigurations, missing patches) on affected assets with active incidents — surfacing exploitable weaknesses that elevate risk and guide containment priorities.', agenticDescription: 'An agent matches observables and affected hosts in a case against the vulnerability inventory, identifies exploitable CVEs aligned with the attack technique, recalculates incident severity, and recommends remediation or compensating controls.', + automationLabel: 'Vulnerability Correlation', + automationCategory: 'cases', automationArea: 'correlation', - customAction: { - label: 'Configure Vulnerabilities', - href: '/vulnerabilities', - description: 'Open the vulnerability inventory to ingest CVEs from your scanners.', - }, + customAction: null, }, { id: 'vulnerability_ingestion_1', phase: 'ingest', source: 'asset_management', target: 'case_management', @@ -1637,7 +1637,7 @@ function mapApiToFrontend(cat: ApiUsecaseCategory, api: ApiUsecase, local?: Usec video: api.video || local?.video, blogpost: api.blogpost || local?.blogpost, referenceImage: api.reference_image || local?.referenceImage, - customAction: api.custom_action || local?.customAction, + customAction: local?.customAction === null ? undefined : (api.custom_action || local?.customAction), }; } @@ -2554,6 +2554,7 @@ const ACTIVE_USECASE_IDS = [ 'threat_intel_network_1', 'threat_intel_edr_1', 'threat_intel_cloud_1', + 'asset_management_case_management_vuln_1', ]; // Small wrapper so UsecaseDetailContent can render an Outcome block without @@ -3334,7 +3335,8 @@ function UsecaseDetailContent({ // itself IS the source, so there is no third-party source auth to // validate. Skip the hard-block for these flows. const isShuffleSourcedFlow = flow.id === 'case_management_cases_forward_1' - || flow.id === 'case_management_communication_1'; + || flow.id === 'case_management_communication_1' + || flow.id === 'asset_management_case_management_vuln_1'; if (willBeEnabled && !hasValidatedSource && !isShuffleSourcedFlow) { // Hard-block the enable. The /workflows/generate endpoint may return // success: true and then quietly skip creating the workflow when no @@ -3369,7 +3371,7 @@ function UsecaseDetailContent({ let validatedSourceAppNames: string[] = []; - if (willBeEnabled) { + if (willBeEnabled && !isShuffleSourcedFlow) { // Step 1+2: re-check live which source tools are VALIDATED right now, // and forward them explicitly to /workflows/generate. Without this // the backend may generate a shell workflow with no source app wired @@ -3413,7 +3415,7 @@ function UsecaseDetailContent({ return; } requestBody.app_name = validatedSourceAppNames.join(','); - } else { + } else if (!willBeEnabled) { // When disabling, the same workflow (e.g. "Ingest Tickets") may also be // powering sibling usecases that share a category (SIEM / EDR / Email // all generate the same ingestion workflow). A blind action_name=remove @@ -3920,7 +3922,8 @@ function UsecaseDetailContent({ // tool. What they DO need is at least one external destination tool // to push to. Point the hint at the Destination side. const isShuffleSourcedFlow = flow.id === 'case_management_cases_forward_1' - || flow.id === 'case_management_communication_1'; + || flow.id === 'case_management_communication_1' + || flow.id === 'asset_management_case_management_vuln_1'; const needsSource = !!flow.source && !selfContained && !isShuffleSourcedFlow; const sourceLabel = flow.source ? categoryLabel(flow.source) : 'source'; // Detect if a source-side app is ALREADY wired into the usecase's @@ -4295,14 +4298,14 @@ function UsecaseDetailContent({ // side and disable adding source tools. const isForwardTickets = flow.id === 'case_management_cases_forward_1'; const isNotifications = flow.id === 'case_management_communication_1'; - const isCasesSourceOnly = isForwardTickets || isNotifications; + const isCasesSourceOnly = isForwardTickets || isNotifications || flow.id === 'asset_management_case_management_vuln_1'; const skipShuffle = endpoint.title === 'Destination' && (isCasesSourceOnly || (flow.source === 'case_management' && !isMultiDest)); const showShuffle = includesCases && !skipShuffle; // When Shuffle itself is the Source, only surface the Shuffle Security // tile — hiding other case-management apps that would otherwise clutter // the source side of the flow. - const sourceIsShuffleOnly = endpoint.title === 'Source' && endpoint.categoryId === 'case_management'; + const sourceIsShuffleOnly = endpoint.title === 'Source' && (endpoint.categoryId === 'case_management' || flow.id === 'asset_management_case_management_vuln_1'); // Some usecases (e.g. Enrichment) run entirely on a built-in Shuffle // workflow — the destination has no third-party apps to wire up, // only the Shuffle Security platform itself. @@ -4358,7 +4361,7 @@ function UsecaseDetailContent({ const appNamesWithShuffle = injected.length ? [...baseAppNames, ...injected] : baseAppNames; - const synthetic = (showShuffle || destIsShuffleOnly) + const synthetic = (showShuffle || destIsShuffleOnly || sourceIsShuffleOnly) ? [{ id: 'shuffle-security', name: 'Shuffle Security', @@ -4426,7 +4429,7 @@ function UsecaseDetailContent({ - {endpoint.title === 'Source' && endpoint.categoryId === 'case_management' ? 'Shuffle' : (endpoint.meta?.label || 'Unknown')} + {sourceIsShuffleOnly ? 'Shuffle' : (endpoint.meta?.label || 'Unknown')} {endpoint.details ? ( { - // Indirect access via `new Function` keeps `import.meta` out of the emitted - // CJS bundle. tsup otherwise inlines it verbatim into `dist/index.js`, which - // breaks consumers whose webpack rolls the CJS build into a non-ESM bundle - // ("Cannot use 'import.meta' outside a module"). + // Dynamic property access avoids tsup's static inlining of import.meta.env.* + // while still working in Vite's dev server and build output. try { - const meta = (new Function('try { return import.meta } catch { return undefined }')()) as - | { env?: Record } - | undefined; - return meta?.env?.[key]; + return (import.meta as any).env?.[key]; } catch { return undefined; } @@ -78,9 +73,7 @@ const getDefaultBaseUrl = (): string => { // Cloud domains always default to shuffler.io; region_url from getinfo may override later if (isCloudDomain()) return PROD_BACKEND; // Self-hosted / on-prem: use current domain (nginx proxies /api/* to backend) - if (typeof window !== 'undefined') { - return window.location.origin; - } + if (typeof window !== 'undefined') return window.location.origin; return PROD_BACKEND; };