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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CrowdStrikeGuide } from '@/features/integrations/components/setup/cloud
import { M365Guide, O365_GRAPH_FIELDS } from '@/features/integrations/components/setup/collector/collectors/M365Guide'
import { SophosGuide } from '@/features/integrations/components/setup/collector/collectors/SophosGuide'
import { CustomSetup } from '@/features/integrations/components/setup/CustomSetup'
import { Section } from '@/features/integrations/components/ui/Section'
import { getIntegrationDashboardSearch } from '@/features/dashboard/utils/integration-dashboard-routing'
import type { Integration } from '@/features/integrations/types'

Expand Down Expand Up @@ -195,7 +196,17 @@ export function IntegrationDrawer({ integration: i, onClose }: IntegrationDrawer
<div className="flex-1 overflow-y-auto bg-muted/20 p-6">
<>
{isCustom(i) && <CustomSetup integration={i} />}
{!isCustom(i) && isAgent(i) && <AgentSetup integration={i} />}
{!isCustom(i) && isAgent(i) && i.guideChannel && <CollectorSetup integration={i} />}
{!isCustom(i) && isAgent(i) && !i.guideChannel && (
<>
<AgentSetup integration={i} />
{i.guideSetting && (
<Section title={i.name}>
<p className="text-sm text-foreground/90">{i.guideSetting}</p>
</Section>
)}
</>
)}
{!isCustom(i) && i.kind === 'cloud' && isAWS(i.name) && <AWSGuide integration={i} />}
{!isCustom(i) && i.kind === 'cloud' && isAzure(i.name) && <AzureGuide integration={i} />}
{!isCustom(i) && i.kind === 'cloud' && isGCP(i) && <GcpGuide integration={i} />}
Expand Down
125 changes: 66 additions & 59 deletions frontend/src/features/integrations/components/setup/ChannelGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type { ReactNode } from 'react'
import { Section } from '@/features/integrations/components/ui/Section'
import { CodeBlock } from '@/features/integrations/components/ui/CodeBlock'
import { AgentSetup } from '@/features/integrations/components/setup/AgentSetup'
import { forwarderHost } from '@/features/integrations/components/setup/collector/ForwarderGuide'
import { ForwarderGuide } from '@/features/integrations/components/setup/collector/ForwarderGuide'
import type { RemoteEnableSelection } from '@/features/integrations/components/setup/collector/RemoteEnablePanel'
import { defaultPortFor } from '@/features/integrations/components/setup/collector/protoCatalog'
import type { Integration, IntegrationGuideChannel } from '@/features/integrations/types'

export function ChannelGuide({ integration }: { integration: Integration }) {
Expand Down Expand Up @@ -41,74 +43,79 @@ function GuideLayout({

function SyslogDeviceGuide({ integration }: { integration: Integration }) {
const { t } = useTranslation()
const [protocol, setProtocol] = useState<'tcp' | 'udp'>('tcp')
const host = forwarderHost()
const sourceType = integration.forwarderDataType || 'syslog'
const port = defaultPortFor(sourceType, 'tcp') || '7014'
const [selection, setSelection] = useState<RemoteEnableSelection>({ proto: 'tcp', port })
const host = selection.collector?.ip || '<collector-host>'

return (
<GuideLayout title={`${integration.name} via Syslog`}>
<p className="text-sm text-foreground/90">
Configure the device to send syslog to the UTMStack Collector. TCP is recommended for chatty
devices; the UDP receiver uses a 2,048-byte message buffer and truncates longer messages.
</p>
<label className="block max-w-40 text-xs text-muted-foreground">
Protocol
<select
value={protocol}
onChange={(event) => setProtocol(event.target.value as 'tcp' | 'udp')}
className="mt-1 h-9 w-full rounded-md border border-border bg-background px-3 text-sm text-foreground"
>
<option value="tcp">TCP (recommended)</option>
<option value="udp">UDP</option>
</select>
</label>
<CodeBlock
lang="config"
code={`Collector: ${host}
Port: 7014
Protocol: ${protocol.toUpperCase()}
<ForwarderGuide
source={integration.name}
sourceType={sourceType}
port={selection.port || port}
defaultPort={port}
defaultProto="tcp"
onSetSelection={setSelection}
>
<Section title={`${integration.name} via Syslog`} step={2}>
<p className="text-sm text-foreground/90">
Configure the device to send syslog to the selected UTMStack Collector. TCP is recommended
for chatty devices; the UDP receiver uses a 2,048-byte message buffer and truncates longer
messages.
</p>
<CodeBlock
lang="config"
code={`Collector: ${host}
Port: ${selection.port || port}
Protocol: ${selection.proto.toUpperCase()}
Format: RFC 5424 or RFC 3164`}
/>
<p className="text-sm text-foreground/90">
For a device that only supports port 514, change the Collector's syslog UDP port with:
</p>
<CodeBlock code="utmstack_forwarder change-port syslog udp 514" />
<p className="text-xs text-muted-foreground">
Only do this on a Collector that is not running Cisco integrations; they may already use UDP
514. The command refuses a port that is busy. Changing a built-in port is not done with
<code className="mx-1 font-mono">enable-integration</code>.
</p>
<p className="text-xs text-muted-foreground">
{t('datasources.drawer.viewLogs')} will open Log Explorer for this integration's data type.
</p>
{integration.guideSetting && (
<p className="text-sm text-foreground/90">{integration.guideSetting}</p>
)}
</GuideLayout>
/>
<p className="text-xs text-muted-foreground">
{t('datasources.drawer.viewLogs')} will open Log Explorer for this integration's data type.
</p>
{integration.guideSetting && (
<p className="text-sm text-foreground/90">{integration.guideSetting}</p>
)}
</Section>
</ForwarderGuide>
)
}

function NetflowExporterGuide({ integration }: { integration: Integration }) {
const host = forwarderHost()
const sourceType = integration.forwarderDataType || 'netflow'
const port = defaultPortFor(sourceType, 'udp') || '2055'
const [selection, setSelection] = useState<RemoteEnableSelection>({ proto: 'udp', port })
const host = selection.collector?.ip || '<collector-host>'

return (
<GuideLayout title={`${integration.name} NetFlow exporter`}>
<p className="text-sm text-foreground/90">
Configure the exporter to send NetFlow over UDP to the UTMStack Collector:
</p>
<CodeBlock
lang="config"
code={`Collector: ${host}
Port: 2055
<ForwarderGuide
source={integration.name}
sourceType={sourceType}
port={selection.port || port}
defaultPort={port}
defaultProto="udp"
onSetSelection={setSelection}
>
<Section title={`${integration.name} NetFlow exporter`} step={2}>
<p className="text-sm text-foreground/90">
Configure the exporter to send NetFlow over UDP to the selected UTMStack Collector:
</p>
<CodeBlock
lang="config"
code={`Collector: ${host}
Port: ${selection.port || port}
Protocol: UDP
Version: NetFlow v9 (where supported)`}
/>
<p className="text-sm text-foreground/90">
Select the export version supported by the device and configure its source interface. Verify
that flows arrive in Log Explorer using the View logs button.
</p>
{integration.guideSetting && (
<p className="text-sm text-foreground/90">{integration.guideSetting}</p>
)}
</GuideLayout>
/>
<p className="text-sm text-foreground/90">
Select the export version supported by the device and configure its source interface. Verify
that flows arrive in Log Explorer using the View logs button.
</p>
{integration.guideSetting && (
<p className="text-sm text-foreground/90">{integration.guideSetting}</p>
)}
</Section>
</ForwarderGuide>
)
}

Expand Down
99 changes: 57 additions & 42 deletions frontend/src/features/integrations/components/setup/CustomSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { useTranslation, Trans } from 'react-i18next'
import { Forward, Server, ShieldCheck } from 'lucide-react'
import { Forward, Server, ShieldCheck } from 'lucide-react'
import { Section } from '@/features/integrations/components/ui/Section'
import { CodeBlock } from '@/features/integrations/components/ui/CodeBlock'
import {
Expand All @@ -9,7 +9,7 @@ import {
FlowEdge,
} from '@/features/integrations/components/setup/collector/ForwarderGuide'
import type { Integration } from '@/features/integrations/types'
import { type RemoteEnableSelection } from './collector/RemoteEnablePanel'
import type { RemoteEnableSelection } from './collector/RemoteEnablePanel'
const ROOT = 'integrations.setup.custom'

// A custom data type has no ProtoPorts/HTTPPorts catalog entry, so every
Expand All @@ -18,7 +18,8 @@ const ROOT = 'integrations.setup.custom'

export function CustomSetup({ integration }: { integration: Integration }) {
const { t } = useTranslation()
const name = integration.dataType || integration.moduleName?.toLowerCase() || 'my-integration'
const dataType = integration.dataType
const name = dataType || integration.moduleName?.toLowerCase() || 'my-integration'

const [selection, setSelection] = useState<RemoteEnableSelection>({ proto: 'udp', port: '7100' })
const { proto, port } = selection
Expand All @@ -28,43 +29,55 @@ export function CustomSetup({ integration }: { integration: Integration }) {
// to the Forwarder host.
const endpoint = `${proto}://127.0.0.1:${port}/logs`

return (
<div className="space-y-4">
<ForwarderGuide port={selection.port} source={`${ROOT}`} sourceType={integration.dataType!}

onSetSelection={setSelection}

header={(
<Section title={t(`${ROOT}.intro.title`)}>
<p className="text-sm text-foreground/90">{t(`${ROOT}.intro.body`)}</p>
<div className="mt-3 flex items-stretch justify-center gap-1 sm:gap-2">
<FlowNode
icon={<Server size={20} className="text-muted-foreground" />}
title={t(`${ROOT}.diagram.source`)}
sub={t(`${ROOT}.diagram.sourceSub`)}
tone="neutral"
/>
<FlowEdge label={selection.proto.toUpperCase()} />
<FlowNode
icon={<Forward size={20} />}
title={t(`${ROOT}.diagram.forwarder`)}
sub={`:${selection.port}`}
tone="accent"
/>
<FlowEdge label={t(`${ROOT}.diagram.ingests`)} />
<FlowNode
icon={<ShieldCheck size={20} />}
title="UTMStack"
sub={t(`${ROOT}.diagram.utmstackSub`)}
tone="brand"
/>
</div>
<p className="mt-3 rounded-md bg-muted/40 px-3 py-2 text-[11px] text-muted-foreground">
{t(`${ROOT}.intro.note`)}
if (!dataType) {
return (
<Section title={t('integrations.setup.collector.guideUnavailableTitle')}>
<p className="text-sm text-foreground/90">
{t('integrations.setup.collector.guideUnavailableBody', { name: integration.name })}
</p>
</Section>
)}
>
)
}

return (
<div className="space-y-4">
<ForwarderGuide
port={selection.port}
defaultPort="7100"
source={integration.name}
sourceType={dataType}
onSetSelection={setSelection}
header={(
<Section title={t(`${ROOT}.intro.title`)}>
<p className="text-sm text-foreground/90">{t(`${ROOT}.intro.body`)}</p>
<div className="mt-3 flex items-stretch justify-center gap-1 sm:gap-2">
<FlowNode
icon={<Server size={20} className="text-muted-foreground" />}
title={t(`${ROOT}.diagram.source`)}
sub={t(`${ROOT}.diagram.sourceSub`)}
tone="neutral"
/>
<FlowEdge label={selection.proto.toUpperCase()} />
<FlowNode
icon={<Forward size={20} />}
title={t(`${ROOT}.diagram.forwarder`)}
sub={`:${selection.port}`}
tone="accent"
/>
<FlowEdge label={t(`${ROOT}.diagram.ingests`)} />
<FlowNode
icon={<ShieldCheck size={20} />}
title="UTMStack"
sub={t(`${ROOT}.diagram.utmstackSub`)}
tone="brand"
/>
</div>
<p className="mt-3 rounded-md bg-muted/40 px-3 py-2 text-[11px] text-muted-foreground">
{t(`${ROOT}.intro.note`)}
</p>
</Section>
)}
>

<Section title={t(`${ROOT}.send.title`)} step={2}>
{isHttp ? (
Expand All @@ -90,7 +103,11 @@ export function CustomSetup({ integration }: { integration: Integration }) {
<p className="text-sm text-foreground/90">
<Trans
i18nKey={`${ROOT}.send.bodyNetwork`}
values={{ host:selection.collector?.ip, port:selection.port, proto: selection.proto.toUpperCase() }}
values={{
host: selection.collector?.ip || '<collector-host>',
port: selection.port,
proto: selection.proto.toUpperCase(),
}}
components={{ hl: <strong className="font-semibold text-primary" /> }}
/>
</p>
Expand All @@ -102,9 +119,7 @@ export function CustomSetup({ integration }: { integration: Integration }) {
<p className="text-sm text-foreground/90">{t(`${ROOT}.parserBody`, { dataType: name })}</p>
</Section>

</ForwarderGuide>

</ForwarderGuide>
</div>
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,14 @@ interface ForwarderGuideProps {
source: string
/** Forwarder port for this data type, e.g. "7016" (illustrative default shown in the diagram). */
port: string
/** Stable initial port fallback when the data type has no catalog entry. */
defaultPort?: string
/** The dataType shown in the connection details, e.g. "ibm-aix". */
sourceType: string
/** Initial protocol, when the device needs something other than the catalog's first entry (e.g. ESXi/SentinelOne are TCP-only in practice). */
defaultProto?: Proto
/** Explicit protocol choices when this integration has a narrower catalog than its data type. */
availableProtos?: Proto[]
/** Vendor-specific device-side config (how to point the device at the Forwarder). */
header?: ReactNode
onSetSelection?:(selection:RemoteEnableSelection)=>void
Expand All @@ -170,18 +174,24 @@ interface ForwarderGuideProps {
export function ForwarderGuide({
source,
port,
defaultPort,
sourceType,
defaultProto,
availableProtos: configuredProtos,
header,
onSetSelection,
children
}: ForwarderGuideProps) {
const { t } = useTranslation()
const availableProtos = useMemo(() => availableProtosFor(sourceType), [sourceType])
const availableProtos = useMemo(
() => configuredProtos ?? availableProtosFor(sourceType),
[sourceType, configuredProtos],
)
const initialProto = defaultProto ?? availableProtos[0]
const fallbackPort = defaultPortFor(sourceType, initialProto) || defaultPort || port
const [selection, setSelection] = useState<RemoteEnableSelection>(() => ({
proto: initialProto,
port: defaultPortFor(sourceType, initialProto) || port,
port: fallbackPort,
apiKey: null,
}))

Expand Down Expand Up @@ -215,6 +225,7 @@ export function ForwarderGuide({
dataType={sourceType}
availableProtos={availableProtos}
defaultProto={defaultProto}
defaultPort={fallbackPort}
step={1}
onSelectionChange={setSelection}
onRequestAddCollector={handleAddCollector}
Expand Down
Loading
Loading