Skip to content

[Phase 1 Issue #7] Frontend Variable Customization UI - Complete Scan Configuration Interface #106

@remyluslosius

Description

@remyluslosius

Overview

Implement the Frontend Variable Customization UI to complete Phase 1 of the hybrid scanning architecture. This provides the user interface for framework selection, variable customization, template management, and scan configuration.

Phase 1 Progress: 6/7 tasks completed → 7/7 after this issue

Problem Statement

The backend APIs (PR #105) provide complete framework discovery, variable management, and template operations, but users have no UI to:

  • Browse available compliance frameworks
  • Customize XCCDF variables with validation
  • Save and reuse scan configurations as templates
  • Configure scans with framework/template selection

Solution: Complete Frontend UI

Implement React/TypeScript components integrated with Material-UI v5 and the scan configuration APIs.

Architecture: Page Organization

1. /content - Compliance Content Hub

Add two new sections:

A. /content/frameworks (NEW)

  • Framework discovery and browsing
  • Variable definitions viewer
  • Framework metadata display

B. /content/templates (NEW)

  • Template list (user's own + public)
  • Template editor (CRUD operations)
  • Template history/audit logs
  • Template statistics

2. /scans/config - Enhanced Scan Configuration

Modify existing page:

  • Framework/version selection
  • Template selection (dropdown)
  • Variable customization (dynamic form)
  • Target selection (existing hosts only)
  • Save as template option

3. /host-groups - Template Quick Actions

Add to existing page:

  • "Scan with Template" dropdown menu
  • Quick template application to groups

Implementation Tasks

1. New Pages (2 pages)

A. Frameworks Page (frontend/src/pages/Content/FrameworksPage.tsx)

Features:

  • Grid/list view of available frameworks
  • Framework cards with metadata (rule count, variable count)
  • Click to view framework details
  • Search/filter frameworks

API Calls:

  • GET /scan-config/frameworks

Components:

<FrameworksPage>
  <PageHeader title="Compliance Frameworks" />
  <SearchBar onSearch={handleSearch} />
  <Grid>
    <FrameworkCard 
      framework="nist"
      displayName="NIST 800-53"
      versions={["rev4", "rev5"]}
      ruleCount={487}
      variableCount={62}
      onViewDetails={handleViewDetails}
    />
    ...
  </Grid>
</FrameworksPage>

B. Framework Details Page (frontend/src/pages/Content/FrameworkDetailPage.tsx)

Features:

  • Framework overview (description, versions)
  • Variable list grouped by category
  • Variable details (type, default, constraints)
  • "Create Template from Framework" button

API Calls:

  • GET /scan-config/frameworks/{framework}/{version}
  • GET /scan-config/frameworks/{framework}/{version}/variables

Components:

<FrameworkDetailPage>
  <Breadcrumbs: Home > Content > Frameworks > NIST 800-53 rev5>
  <FrameworkHeader framework={framework} version={version} />
  <Tabs>
    <Tab label="Overview">
      <FrameworkOverview />
    </Tab>
    <Tab label="Variables (62)">
      <VariableList 
        variables={variables}
        groupBy="category"
      />
    </Tab>
    <Tab label="Rules (487)">
      <RuleList framework={framework} version={version} />
    </Tab>
  </Tabs>
  <Button onClick={handleCreateTemplate}>
    Create Template from Framework
  </Button>
</FrameworkDetailPage>

C. Templates Page (frontend/src/pages/Content/TemplatesPage.tsx)

Features:

  • List user's templates + public templates
  • Filter by framework, tags, visibility
  • Template cards with metadata
  • CRUD actions (Edit, Delete, Clone, Set Default)
  • Template statistics

API Calls:

  • GET /scan-config/templates
  • POST /scan-config/templates/{id}/clone
  • DELETE /scan-config/templates/{id}
  • POST /scan-config/templates/{id}/set-default

Components:

<TemplatesPage>
  <PageHeader 
    title="Scan Configuration Templates"
    action={<Button onClick={handleNew}>New Template</Button>}
  />
  <FilterBar 
    frameworks={frameworks}
    onFilter={handleFilter}
  />
  <Section title="My Templates (8)">
    <TemplateCard
      template={template}
      isDefault={template.is_default}
      onEdit={handleEdit}
      onClone={handleClone}
      onDelete={handleDelete}
      onSetDefault={handleSetDefault}
      onUse={handleUse}
    />
  </Section>
  <Section title="Public Templates (12)">
    <TemplateCard
      template={publicTemplate}
      isPublic
      onClone={handleClone}
      onView={handleView}
    />
  </Section>
</TemplatesPage>

D. Template Editor Page (frontend/src/pages/Content/TemplateEditorPage.tsx)

Features:

  • Template name, description, tags
  • Framework/version selection
  • Variable customization form
  • Rule filter options
  • Save/Update/Cancel actions
  • Audit log tab (for existing templates)

API Calls:

  • POST /scan-config/templates (create)
  • PUT /scan-config/templates/{id} (update)
  • GET /scan-config/templates/{id} (load)
  • POST /scan-config/frameworks/{framework}/{version}/validate (validation)

Components:

<TemplateEditorPage>
  <PageHeader title={isEdit ? "Edit Template" : "New Template"} />
  <Form onSubmit={handleSubmit}>
    <TextField label="Name" value={name} onChange={setName} />
    <TextField label="Description" multiline value={description} />
    
    <FrameworkSelector 
      value={framework}
      onChange={handleFrameworkChange}
    />
    
    <VariableCustomizer
      framework={framework}
      version={version}
      initialValues={template?.variable_overrides}
      onChange={handleVariablesChange}
      onValidate={handleValidate}
    />
    
    <RuleFilterEditor
      value={ruleFilter}
      onChange={setRuleFilter}
    />
    
    <TagInput value={tags} onChange={setTags} />
    
    <FormControlLabel
      control={<Checkbox checked={isDefault} onChange={setIsDefault} />}
      label="Set as default template"
    />
    
    <FormControlLabel
      control={<Checkbox checked={isPublic} onChange={setIsPublic} />}
      label="Make public"
    />
    
    <Button onClick={handleCancel}>Cancel</Button>
    <Button type="submit" variant="contained">Save Template</Button>
  </Form>
  
  {isEdit && (
    <Tabs>
      <Tab label="Configuration" />
      <Tab label="History">
        <AuditLog templateId={template.template_id} />
      </Tab>
    </Tabs>
  )}
</TemplateEditorPage>

2. Modified Pages (2 pages)

A. Enhanced /scans/config (frontend/src/pages/Scans/ConfigPage.tsx)

Modifications:

  • Add "From Template" tab
  • Add framework/version selection
  • Add variable customization accordion
  • Add "Save as template" checkbox
  • Integrate with host selection (existing hosts only)

API Calls:

  • GET /scan-config/frameworks
  • GET /scan-config/frameworks/{framework}/{version}/variables
  • POST /scan-config/frameworks/{framework}/{version}/validate
  • GET /scan-config/templates
  • POST /scan-config/templates/{id}/apply
  • POST /scans/execute

Components:

<ScanConfigPage>
  <Tabs value={activeTab} onChange={setActiveTab}>
    <Tab label="Quick Scan" />
    <Tab label="From Template" />
    <Tab label="Advanced" />
  </Tabs>
  
  {activeTab === 0 && (
    <QuickScanTab>
      <FrameworkSelector 
        onChange={handleFrameworkChange}
      />
      
      <Accordion>
        <AccordionSummary>
          Show Variables ({variables.length})
        </AccordionSummary>
        <AccordionDetails>
          <VariableCustomizer
            framework={framework}
            version={version}
            onChange={handleVariablesChange}
          />
        </AccordionDetails>
      </Accordion>
      
      <TargetSelector
        hosts={hosts}
        hostGroups={hostGroups}
        onSelectHost={setSelectedHost}
        onSelectGroup={setSelectedGroup}
        onAddNewHost={() => navigate('/hosts/new')}
      />
      
      <FormControlLabel
        control={<Checkbox />}
        label="Save as template"
      />
      
      <Button onClick={handleRunScan}>Run Scan</Button>
    </QuickScanTab>
  )}
  
  {activeTab === 1 && (
    <TemplateTab>
      <TemplateSelector
        templates={templates}
        onChange={handleTemplateChange}
      />
      
      <TemplatePreview template={selectedTemplate} />
      
      <Accordion>
        <AccordionSummary>Override Variables</AccordionSummary>
        <AccordionDetails>
          <VariableCustomizer
            framework={selectedTemplate.framework}
            version={selectedTemplate.framework_version}
            initialValues={selectedTemplate.variable_overrides}
            onChange={handleOverrides}
          />
        </AccordionDetails>
      </Accordion>
      
      <TargetSelector ... />
      
      <Button onClick={handleRunScan}>Run Scan</Button>
    </TemplateTab>
  )}
</ScanConfigPage>

B. Enhanced /host-groups (frontend/src/pages/HostGroups/HostGroupsPage.tsx)

Modifications:

  • Add "Scan with Template" dropdown to each host group card
  • Quick template application flow

Components:

<HostGroupCard>
  <CardHeader
    title={group.name}
    subheader={`${group.host_count} hosts`}
    action={
      <Menu>
        <MenuItem onClick={handleViewHosts}>View Hosts</MenuItem>
        <MenuItem onClick={handleEdit}>Edit Group</MenuItem>
        <Divider />
        <MenuItem>
          <ListItemText primary="Scan with Template" />
          <ArrowRight />
        </MenuItem>
        {/* Submenu */}
        <SubMenu>
          {templates.map(t => (
            <MenuItem onClick={() => handleScanWithTemplate(group, t)}>
              {t.name}
            </MenuItem>
          ))}
          <Divider />
          <MenuItem onClick={handleCustomScan}>Custom Configuration...</MenuItem>
        </SubMenu>
      </Menu>
    }
  />
</HostGroupCard>

3. Reusable Components (12 new components)

A. FrameworkSelector (frontend/src/components/Frameworks/FrameworkSelector.tsx)

Purpose: Dropdown for framework + version selection

interface FrameworkSelectorProps {
  value?: { framework: string; version: string }
  onChange: (framework: string, version: string) => void
  disabled?: boolean
}

export const FrameworkSelector = ({ value, onChange, disabled }) => {
  const { data: frameworks } = useFrameworks()
  const [selectedFramework, setSelectedFramework] = useState(value?.framework)
  const [selectedVersion, setSelectedVersion] = useState(value?.version)
  
  const selectedFrameworkData = frameworks?.find(f => f.framework === selectedFramework)
  
  return (
    <Box>
      <Autocomplete
        options={frameworks || []}
        getOptionLabel={(f) => f.display_name}
        value={selectedFrameworkData}
        onChange={(e, f) => {
          setSelectedFramework(f?.framework)
          onChange(f?.framework, f?.versions[0])
        }}
        disabled={disabled}
        renderInput={(params) => <TextField {...params} label="Framework" />}
      />
      
      {selectedFramework && (
        <Autocomplete
          options={selectedFrameworkData?.versions || []}
          value={selectedVersion}
          onChange={(e, v) => {
            setSelectedVersion(v)
            onChange(selectedFramework, v)
          }}
          disabled={disabled}
          renderInput={(params) => <TextField {...params} label="Version" />}
        />
      )}
    </Box>
  )
}

B. VariableCustomizer (frontend/src/components/Variables/VariableCustomizer.tsx)

Purpose: Dynamic form for variable customization with validation

Features:

  • Grouped by category (collapsible accordions)
  • Type-specific inputs (number, select, text)
  • Real-time validation
  • Default value display
  • Constraint indicators
interface VariableCustomizerProps {
  framework: string
  version: string
  initialValues?: Record<string, string>
  onChange: (variables: Record<string, string>) => void
  onValidate?: (isValid: boolean, errors: Record<string, string>) => void
}

export const VariableCustomizer = ({ 
  framework, 
  version, 
  initialValues, 
  onChange,
  onValidate 
}) => {
  const { data: variables } = useFrameworkVariables(framework, version)
  const [values, setValues] = useState(initialValues || {})
  const [errors, setErrors] = useState<Record<string, string>>({})
  
  // Group variables by category
  const groupedVariables = useMemo(() => {
    return groupBy(variables, 'category')
  }, [variables])
  
  // Validate on change
  const handleChange = async (varId: string, value: any) => {
    const newValues = { ...values, [varId]: value }
    setValues(newValues)
    onChange(newValues)
    
    // Validate
    const validation = await frameworkService.validateVariables(
      framework, 
      version, 
      newValues
    )
    setErrors(validation.errors || {})
    onValidate?.(validation.valid, validation.errors)
  }
  
  return (
    <Box>
      {Object.entries(groupedVariables).map(([category, vars]) => (
        <Accordion key={category}>
          <AccordionSummary expandIcon={<ExpandMoreIcon />}>
            <Typography>{category || 'General'} ({vars.length})</Typography>
          </AccordionSummary>
          <AccordionDetails>
            <Stack spacing={2}>
              {vars.map(variable => (
                <VariableInput
                  key={variable.id}
                  variable={variable}
                  value={values[variable.id]}
                  onChange={(v) => handleChange(variable.id, v)}
                  error={errors[variable.id]}
                />
              ))}
            </Stack>
          </AccordionDetails>
        </Accordion>
      ))}
    </Box>
  )
}

C. VariableInput (frontend/src/components/Variables/VariableInput.tsx)

Purpose: Type-specific input for a single variable

interface VariableInputProps {
  variable: VariableDefinition
  value: any
  onChange: (value: any) => void
  error?: string
}

export const VariableInput = ({ variable, value, onChange, error }) => {
  const currentValue = value ?? variable.default
  
  // Render based on type and constraints
  if (variable.type === 'number') {
    const { lower_bound, upper_bound } = variable.constraints || {}
    
    return (
      <Box>
        <Typography variant="subtitle2">{variable.title}</Typography>
        <Typography variant="caption" color="text.secondary">
          {variable.description}
        </Typography>
        
        {lower_bound !== undefined && upper_bound !== undefined ? (
          <Box>
            <Slider
              value={Number(currentValue)}
              onChange={(e, v) => onChange(v)}
              min={lower_bound}
              max={upper_bound}
              marks
              valueLabelDisplay="auto"
            />
            <TextField
              type="number"
              value={currentValue}
              onChange={(e) => onChange(e.target.value)}
              inputProps={{ min: lower_bound, max: upper_bound }}
              error={!!error}
              helperText={error || `Range: ${lower_bound}-${upper_bound}`}
            />
          </Box>
        ) : (
          <TextField
            type="number"
            value={currentValue}
            onChange={(e) => onChange(e.target.value)}
            error={!!error}
            helperText={error || `Default: ${variable.default}`}
          />
        )}
      </Box>
    )
  }
  
  if (variable.type === 'boolean') {
    return (
      <FormControlLabel
        control={
          <Switch
            checked={currentValue === 'true' || currentValue === true}
            onChange={(e) => onChange(e.target.checked)}
          />
        }
        label={
          <Box>
            <Typography variant="subtitle2">{variable.title}</Typography>
            <Typography variant="caption" color="text.secondary">
              {variable.description}
            </Typography>
          </Box>
        }
      />
    )
  }
  
  if (variable.constraints?.choices) {
    return (
      <FormControl fullWidth error={!!error}>
        <InputLabel>{variable.title}</InputLabel>
        <Select
          value={currentValue}
          onChange={(e) => onChange(e.target.value)}
          label={variable.title}
        >
          {variable.constraints.choices.map(choice => (
            <MenuItem key={choice} value={choice}>{choice}</MenuItem>
          ))}
        </Select>
        <FormHelperText>
          {error || variable.description}
        </FormHelperText>
      </FormControl>
    )
  }
  
  // Default: text input
  return (
    <TextField
      fullWidth
      label={variable.title}
      value={currentValue}
      onChange={(e) => onChange(e.target.value)}
      helperText={error || variable.description}
      error={!!error}
      inputProps={
        variable.constraints?.match 
          ? { pattern: variable.constraints.match } 
          : {}
      }
    />
  )
}

D. TemplateSelector (frontend/src/components/Templates/TemplateSelector.tsx)

interface TemplateSelectorProps {
  value?: string
  onChange: (templateId: string) => void
  framework?: string
}

export const TemplateSelector = ({ value, onChange, framework }) => {
  const { data: templates } = useTemplates({ framework })
  
  // Group by user's vs public
  const myTemplates = templates?.filter(t => t.created_by === currentUser.username)
  const publicTemplates = templates?.filter(t => t.is_public)
  
  return (
    <Autocomplete
      options={[
        { label: 'My Templates', options: myTemplates || [] },
        { label: 'Public Templates', options: publicTemplates || [] }
      ]}
      groupBy={(option) => option.label}
      getOptionLabel={(t) => t.name}
      value={templates?.find(t => t.template_id === value)}
      onChange={(e, t) => onChange(t?.template_id)}
      renderInput={(params) => <TextField {...params} label="Select Template" />}
      renderOption={(props, template) => (
        <li {...props}>
          <Box>
            <Typography>
              {template.name}
              {template.is_default && <StarIcon fontSize="small" color="primary" />}
            </Typography>
            <Typography variant="caption" color="text.secondary">
              {template.framework} {template.framework_version} | 
              {Object.keys(template.variable_overrides).length} variables
            </Typography>
          </Box>
        </li>
      )}
    />
  )
}

E. TemplateCard (frontend/src/components/Templates/TemplateCard.tsx)

interface TemplateCardProps {
  template: ScanTemplate
  onEdit?: () => void
  onDelete?: () => void
  onClone?: () => void
  onSetDefault?: () => void
  onUse?: () => void
  isPublic?: boolean
}

export const TemplateCard = ({ template, onEdit, onDelete, onClone, onSetDefault, onUse }) => {
  return (
    <Card>
      <CardHeader
        title={
          <Box display="flex" alignItems="center" gap={1}>
            {template.name}
            {template.is_default && <Chip label="Default" size="small" color="primary" />}
            {template.is_public && <PublicIcon fontSize="small" />}
          </Box>
        }
        subheader={`${template.framework} ${template.framework_version}`}
        action={
          <IconButton>
            <MoreVertIcon />
          </IconButton>
        }
      />
      <CardContent>
        <Typography variant="body2" color="text.secondary">
          {template.description}
        </Typography>
        
        <Box mt={2}>
          <Typography variant="caption">
            {Object.keys(template.variable_overrides).length} variables customized
          </Typography>
        </Box>
        
        <Box mt={1}>
          {template.tags.map(tag => (
            <Chip key={tag} label={tag} size="small" sx={{ mr: 0.5 }} />
          ))}
        </Box>
        
        <Box mt={2} display="flex" gap={1}>
          <Typography variant="caption" color="text.secondary">
            Created: {formatDate(template.created_at)}
          </Typography>
        </Box>
      </CardContent>
      <CardActions>
        {onUse && <Button size="small" onClick={onUse}>Use Template</Button>}
        {onEdit && <Button size="small" onClick={onEdit}>Edit</Button>}
        {onClone && <Button size="small" onClick={onClone}>Clone</Button>}
        {onSetDefault && !template.is_default && (
          <Button size="small" onClick={onSetDefault}>Set Default</Button>
        )}
        {onDelete && <Button size="small" color="error" onClick={onDelete}>Delete</Button>}
      </CardActions>
    </Card>
  )
}

4. API Services (2 new services)

A. Framework Service (frontend/src/services/frameworkService.ts)

import { api } from './api'

export const frameworkService = {
  listFrameworks: async () => {
    const response = await api.get('/scan-config/frameworks')
    return response.data
  },
  
  getFrameworkDetails: async (framework: string, version: string) => {
    const response = await api.get(`/scan-config/frameworks/${framework}/${version}`)
    return response.data
  },
  
  getVariables: async (framework: string, version: string) => {
    const response = await api.get(`/scan-config/frameworks/${framework}/${version}/variables`)
    return response.data
  },
  
  validateVariables: async (
    framework: string, 
    version: string, 
    variables: Record<string, any>
  ) => {
    const response = await api.post(
      `/scan-config/frameworks/${framework}/${version}/validate`,
      { variables }
    )
    return response.data
  }
}

B. Template Service (frontend/src/services/templateService.ts)

export const templateService = {
  list: async (params?: { framework?: string; tags?: string }) => {
    const response = await api.get('/scan-config/templates', { params })
    return response.data
  },
  
  get: async (id: string) => {
    const response = await api.get(`/scan-config/templates/${id}`)
    return response.data
  },
  
  create: async (data: CreateTemplateRequest) => {
    const response = await api.post('/scan-config/templates', data)
    return response.data
  },
  
  update: async (id: string, data: UpdateTemplateRequest) => {
    const response = await api.put(`/scan-config/templates/${id}`, data)
    return response.data
  },
  
  delete: async (id: string) => {
    await api.delete(`/scan-config/templates/${id}`)
  },
  
  apply: async (id: string, target: any, additionalOverrides?: Record<string, string>) => {
    const response = await api.post(`/scan-config/templates/${id}/apply`, {
      target,
      variable_overrides: additionalOverrides
    })
    return response.data
  },
  
  clone: async (id: string, newName: string) => {
    const response = await api.post(
      `/scan-config/templates/${id}/clone?new_name=${encodeURIComponent(newName)}`
    )
    return response.data
  },
  
  setDefault: async (id: string) => {
    const response = await api.post(`/scan-config/templates/${id}/set-default`)
    return response.data
  },
  
  getStatistics: async () => {
    const response = await api.get('/scan-config/statistics')
    return response.data
  }
}

5. React Hooks (4 new hooks)

A. useFrameworks (frontend/src/hooks/useFrameworks.ts)

import { useQuery } from '@tanstack/react-query'
import { frameworkService } from '@/services/frameworkService'

export const useFrameworks = () => {
  return useQuery({
    queryKey: ['frameworks'],
    queryFn: () => frameworkService.listFrameworks(),
    staleTime: 5 * 60 * 1000 // 5 minutes
  })
}

export const useFrameworkDetails = (framework: string, version: string) => {
  return useQuery({
    queryKey: ['framework', framework, version],
    queryFn: () => frameworkService.getFrameworkDetails(framework, version),
    enabled: !!framework && !!version
  })
}

export const useFrameworkVariables = (framework: string, version: string) => {
  return useQuery({
    queryKey: ['framework-variables', framework, version],
    queryFn: () => frameworkService.getVariables(framework, version),
    enabled: !!framework && !!version
  })
}

B. useTemplates (frontend/src/hooks/useTemplates.ts)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { templateService } from '@/services/templateService'

export const useTemplates = (filters?: { framework?: string; tags?: string }) => {
  return useQuery({
    queryKey: ['templates', filters],
    queryFn: () => templateService.list(filters)
  })
}

export const useTemplate = (id: string) => {
  return useQuery({
    queryKey: ['template', id],
    queryFn: () => templateService.get(id),
    enabled: !!id
  })
}

export const useCreateTemplate = () => {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: templateService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['templates'] })
    }
  })
}

export const useUpdateTemplate = () => {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: any }) => 
      templateService.update(id, data),
    onSuccess: (_, variables) => {
      queryClient.invalidateQueries({ queryKey: ['templates'] })
      queryClient.invalidateQueries({ queryKey: ['template', variables.id] })
    }
  })
}

export const useDeleteTemplate = () => {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: templateService.delete,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['templates'] })
    }
  })
}

6. TypeScript Types (frontend/src/types/scanConfig.ts)

export interface Framework {
  framework: string
  display_name: string
  versions: string[]
  description: string
  rule_count: number
  variable_count: number
  categories?: string[]
  severities?: Record<string, number>
}

export interface VariableConstraint {
  lower_bound?: number
  upper_bound?: number
  choices?: string[]
  match?: string
}

export interface VariableDefinition {
  id: string
  title: string
  description: string
  type: 'string' | 'number' | 'boolean'
  default: any
  constraints?: VariableConstraint
  interactive: boolean
  category?: string
}

export interface ScanTemplate {
  template_id: string
  name: string
  description?: string
  framework: string
  framework_version: string
  target_type: string
  variable_overrides: Record<string, string>
  rule_filter?: Record<string, any>
  created_by: string
  created_at: string
  updated_at: string
  is_default: boolean
  is_public: boolean
  tags: string[]
  version: number
  shared_with: string[]
}

export interface CreateTemplateRequest {
  name: string
  description?: string
  framework: string
  framework_version: string
  target_type: string
  variable_overrides: Record<string, string>
  rule_filter?: Record<string, any>
  tags: string[]
  is_public: boolean
}

export interface ValidationResult {
  valid: boolean
  errors: Record<string, string>
  warnings: Record<string, string>
}

7. Navigation Updates

A. Sidebar (frontend/src/components/Layout/Sidebar.tsx)

Add menu items:

{
  title: 'Content',
  items: [
    { label: 'Compliance Rules', path: '/content/rules', icon: <RuleIcon /> },
    { label: 'Frameworks', path: '/content/frameworks', icon: <AccountTreeIcon /> }, // NEW
    { label: 'Templates', path: '/content/templates', icon: <BookmarkIcon /> }, // NEW
  ]
}

B. Routes (frontend/src/App.tsx)

<Route path="/content/frameworks" element={<FrameworksPage />} />
<Route path="/content/frameworks/:framework/:version" element={<FrameworkDetailPage />} />
<Route path="/content/templates" element={<TemplatesPage />} />
<Route path="/content/templates/new" element={<TemplateEditorPage />} />
<Route path="/content/templates/:id" element={<TemplateEditorPage />} />

Estimated Effort

5-7 days (as planned)

  • Pages: 2 days (4 new pages)
  • Components: 2 days (12 components)
  • Services/Hooks: 0.5 day
  • Integration: 1 day
  • Testing: 0.5 day
  • Polish: 1 day

Acceptance Criteria

  • Users can browse available frameworks
  • Users can view framework details and variables
  • Users can create templates from frameworks
  • Users can customize variables with validation
  • Users can save/load/edit/delete templates
  • Users can set default templates
  • Users can clone public templates
  • Users can configure scans from templates
  • Users can apply templates to host groups
  • Variable inputs respect type and constraints
  • Real-time validation provides error feedback
  • Audit logs visible on template detail pages
  • All Material-UI components styled consistently

Testing Strategy

Unit Tests

  • Variable validation logic
  • Type-specific input rendering
  • Template CRUD operations
  • Framework selector state management

Integration Tests

  • Complete scan configuration workflow
  • Template creation from framework
  • Template application to scan
  • Variable override merging

E2E Tests

  1. Browse frameworks → View details → Create template
  2. Configure new scan → Customize variables → Save as template → Execute scan
  3. Load template → Override variables → Execute scan
  4. Clone public template → Edit → Use for scan

Related Issues

Phase 1 Completion

After this issue:
Phase 1 Complete (7/7 tasks - 100%):

  1. ✅ Enhanced ComplianceRule Model (PR [Phase 1] Add XCCDFVariable Model and XCCDF Variables Support #95)
  2. ✅ Enhanced SCAP Converter (PR [Phase 1] Enhanced SCAP Converter with Variable and Remediation Extraction #97)
  3. ✅ XCCDF Generator (PR [Phase 1] XCCDF Data-Stream Generator from MongoDB #99)
  4. ✅ Scan Service (PR [Phase 1 Issue #4] MongoDB-Based Scan Service with Multi-Scanner Routing #101)
  5. ✅ ORSA Remediation Engine (PR [Phase 1 Issue #5] ORSA Remediation Engine - Ansible & Bash Executors #103)
  6. ✅ Scan Configuration API (PR [Phase 1 Issue #6] Scan Configuration API - Framework Discovery & Template Management #105)
  7. Frontend Variable Customization UI (this issue)

🎉 Phase 1: XCCDF Variables + Hybrid Scanning Engine - COMPLETE

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions