import { useState, useCallback } from 'react';
import {
useConfig,
useSources,
useSinks,
useSanitizers,
useTerminators,
useProfiles,
} from '../api/queries/config';
import {
useAddSource,
useDeleteSource,
useAddSink,
useDeleteSink,
useAddSanitizer,
useDeleteSanitizer,
useAddTerminator,
useDeleteTerminator,
useAddProfile,
useDeleteProfile,
useActivateProfile,
useToggleTriageSync,
} from '../api/mutations/config';
import { LoadingState } from '../components/ui/LoadingState';
import { ErrorState } from '../components/ui/ErrorState';
import type { LabelEntryView, TerminatorView, ProfileView } from '../api/types';
const LANG_OPTIONS = [
'javascript',
'typescript',
'python',
'go',
'java',
'c',
'cpp',
'php',
'ruby',
'rust',
];
const CAP_OPTIONS = [
'all',
'env_var',
'html_escape',
'shell_escape',
'url_encode',
'json_parse',
'file_io',
'sql_query',
'deserialize',
'ssrf',
'code_exec',
'crypto',
];
// ── Collapsible Config Section ───────────────────────────────────────────────
function ConfigSection({
title,
id,
children,
}: {
title: string;
id: string;
children: React.ReactNode;
}) {
const [collapsed, setCollapsed] = useState(false);
return (
setCollapsed(!collapsed)}
>
▼
{' '}
{title}
{children}
);
}
// ── Label Table (Source/Sink/Sanitizer) ──────────────────────────────────────
function LabelSection({
title,
id,
kind,
entries,
onAdd,
onDelete,
}: {
title: string;
id: string;
kind: string;
entries: LabelEntryView[];
onAdd: (body: { lang: string; matchers: string[]; cap: string }) => void;
onDelete: (entry: LabelEntryView) => void;
}) {
const [lang, setLang] = useState('');
const [matcher, setMatcher] = useState('');
const [cap, setCap] = useState('all');
const builtins = entries.filter((e) => e.is_builtin);
const custom = entries.filter((e) => !e.is_builtin);
const handleAdd = useCallback(() => {
if (!lang || !matcher) return;
onAdd({ lang, matchers: [matcher], cap });
setMatcher('');
}, [lang, matcher, cap, onAdd]);
return (
setMatcher(e.target.value)}
/>
{entries.length === 0 ? (
) : (
| Language |
Matchers |
Cap |
|
{builtins.map((e, i) => (
| {e.lang} |
{e.matchers.join(', ')}
|
{e.cap} |
built-in
|
))}
{custom.map((e, i) => (
| {e.lang} |
{e.matchers.join(', ')}
|
{e.cap} |
|
))}
)}
);
}
// ── Config Page ──────────────────────────────────────────────────────────────
export function ConfigPage() {
const {
data: config,
isLoading: configLoading,
error: configError,
} = useConfig();
const { data: sources } = useSources();
const { data: sinks } = useSinks();
const { data: sanitizers } = useSanitizers();
const { data: terminators } = useTerminators();
const { data: profiles } = useProfiles();
const addSource = useAddSource();
const deleteSource = useDeleteSource();
const addSink = useAddSink();
const deleteSink = useDeleteSink();
const addSanitizer = useAddSanitizer();
const deleteSanitizer = useDeleteSanitizer();
const addTerminator = useAddTerminator();
const deleteTerminator = useDeleteTerminator();
const addProfile = useAddProfile();
const deleteProfile = useDeleteProfile();
const activateProfile = useActivateProfile();
const toggleTriageSync = useToggleTriageSync();
const [termLang, setTermLang] = useState('');
const [termName, setTermName] = useState('');
const [profileName, setProfileName] = useState('');
const handleAddTerminator = useCallback(() => {
if (!termLang || !termName) return;
addTerminator.mutate({ lang: termLang, name: termName });
setTermName('');
}, [termLang, termName, addTerminator]);
const handleSaveProfile = useCallback(() => {
if (!profileName) return;
addProfile.mutate({ name: profileName, settings: {} });
setProfileName('');
}, [profileName, addProfile]);
if (configLoading) return ;
if (configError) return ;
// Extract config fields (config is typed as unknown since it's the raw NyxConfig)
const cfg = config as Record> | undefined;
const scanner = cfg?.scanner as Record | undefined;
const output = cfg?.output as Record | undefined;
const server = cfg?.server as Record | undefined;
return (
<>
Config
{/* General Section */}
Analysis Mode: {String(scanner?.mode || 'full')}
Min Severity:{' '}
{String(scanner?.min_severity || 'Low')}
Max File Size:{' '}
{scanner?.max_file_size_mb
? String(scanner.max_file_size_mb) + ' MB'
: 'unlimited'}
Excluded Dirs:{' '}
{((scanner?.excluded_directories as string[]) || []).join(', ')}
Excluded Exts:{' '}
{((scanner?.excluded_extensions as string[]) || []).join(', ')}
Attack Surface Ranking:{' '}
{output?.attack_surface_ranking ? 'Enabled' : 'Disabled'}
{/* Sources */}
addSource.mutate(body)}
onDelete={(e) =>
deleteSource.mutate({
lang: e.lang,
matchers: e.matchers,
cap: e.cap,
})
}
/>
{/* Sinks */}
addSink.mutate(body)}
onDelete={(e) =>
deleteSink.mutate({ lang: e.lang, matchers: e.matchers, cap: e.cap })
}
/>
{/* Sanitizers */}
addSanitizer.mutate(body)}
onDelete={(e) =>
deleteSanitizer.mutate({
lang: e.lang,
matchers: e.matchers,
cap: e.cap,
})
}
/>
{/* Terminators */}
{!terminators || terminators.length === 0 ? (
No terminators configured
) : (
| Language |
Name |
|
{(terminators as TerminatorView[]).map((t, i) => (
| {t.lang} |
{t.name} |
|
))}
)}
{/* Profiles */}
{!profiles || profiles.length === 0 ? (
) : (
| Name |
Type |
Settings |
|
{(profiles as ProfileView[]).map((p) => (
|
{p.name}
|
{p.is_builtin ? (
built-in
) : (
custom
)}
|
{JSON.stringify(p.settings)}
|
{!p.is_builtin && (
)}
|
))}
)}
>
);
}