Skip to content
Open
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
11 changes: 3 additions & 8 deletions src/Shuffle-Core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined> }
| undefined;
return meta?.env?.[key];
return (import.meta as any).env?.[key];
} catch {
return undefined;
}
Expand Down
13 changes: 6 additions & 7 deletions src/Shuffle-Core/config/usecases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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',
Expand Down
35 changes: 19 additions & 16 deletions src/Shuffle-Core/views/Usecases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -485,6 +485,7 @@ const DESTINATION_SHUFFLE_ONLY_FLOW_IDS = new Set<string>([
'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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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),
};
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -4426,7 +4429,7 @@ function UsecaseDetailContent({
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>

<Typography sx={{ fontSize: '0.85rem', fontWeight: 700, color: FG, letterSpacing: '0.02em', flexShrink: 0 }}>
{endpoint.title === 'Source' && endpoint.categoryId === 'case_management' ? 'Shuffle' : (endpoint.meta?.label || 'Unknown')}
{sourceIsShuffleOnly ? 'Shuffle' : (endpoint.meta?.label || 'Unknown')}
</Typography>
{endpoint.details ? (
<Typography sx={{
Expand Down
15 changes: 4 additions & 11 deletions src/Shuffle-MCPs/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,10 @@ const CLOUD_DOMAINS = ['security.shuffler.io', 'shutdown.no'];
// Safely read Vite-style env vars without depending on `vite/client` types
// (the published library should not require Vite to be installed).
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<string, string | undefined> }
| undefined;
return meta?.env?.[key];
return (import.meta as any).env?.[key];
} catch {
return undefined;
}
Expand Down Expand Up @@ -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;
};

Expand Down