React-based web interface for the SecuScan pentesting toolkit.
- Node.js 18+ and npm
- SecuScan backend running on
http://127.0.0.1:8080
# Install dependencies
npm install
# Start development server
npm run devThe frontend will start on http://localhost:3000 with hot module replacement enabled.
# Build for production
npm run build
# Preview production build
npm run previewfrontend/
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── AppShell.tsx # Main app layout with sidebar
│ │ ├── ConsentModal.tsx # Consent confirmation dialog
│ │ ├── DynamicForm.tsx # Form generator from plugin schema
│ │ └── TaskCard.tsx # Task display card
│ │
│ ├── pages/ # Route components
│ │ ├── Scanner.tsx # Main scanning interface
│ │ ├── Scans.tsx # Task history list
│ │ ├── TaskDetails.tsx # Individual task view
│ │ └── Settings.tsx # Settings page
│ │
│ ├── context/ # React Context for state
│ │ └── AppContext.tsx # Global app state
│ │
│ ├── services/ # API integration
│ │ └── api.ts # Backend API client
│ │
│ ├── App.tsx # Main app component with routing
│ ├── App.css # App-specific styles
│ ├── main.tsx # React entry point
│ └── index.css # Global styles
│
├── index.html # HTML template
├── vite.config.ts # Vite configuration
├── package.json # Dependencies
└── README.md # This file
- Framework: React 18
- Build Tool: Vite 5
- Router: React Router v6
- State: React Context + Hooks
- Styling: CSS Modules (vanilla CSS)
- API: Fetch API
- Small, reusable components
- Props for configuration
- Separation of concerns
AppContextprovides plugins and settings- Avoids prop drilling
- Centralized data fetching
api.tsabstracts backend communication- Consistent error handling
- Type-safe responses (via Pydantic backend)
- Forms generated from plugin metadata
- Supports conditional fields (
show_if) - Preset-based defaults
Sidebar lists all available plugins fetched from backend:
<Layout> {/* Sidebar with plugin list */}
<Scanner /> {/* Dynamic form for selected plugin */}
</Layout>Forms are generated from plugin schema metadata:
{
"fields": [
{
"name": "target",
"type": "text",
"label": "Target IP",
"required": true,
"placeholder": "192.168.1.1"
}
]
}Rendered as:
<DynamicForm schema={schema} onSubmit={handleSubmit} />Before starting intrusive scans, users must confirm:
<ConsentModal
plugin={plugin}
onConfirm={() => api.startTask(...)}
onCancel={() => setShowConsent(false)}
/>Task details page auto-refreshes every 2 seconds while task is running:
useEffect(() => {
const interval = setInterval(() => {
if (task?.status === 'running') loadTask()
}, 2000)
return () => clearInterval(interval)
}, [task?.status])Displays all tasks with filtering and auto-refresh:
// Auto-refresh every 5 seconds
useEffect(() => {
const interval = setInterval(loadTasks, 5000)
return () => clearInterval(interval)
}, [])- Create component in
src/pages/:
// src/pages/MyPage.tsx
export default function MyPage() {
return <div>My Page Content</div>
}- Add route in
App.tsx:
<Route path="/mypage" element={<MyPage />} />- Add navigation link in your layout component (e.g.
AppShell.tsx):
<NavLink to="/mypage">My Page</NavLink>- Create component in
src/components/:
// src/components/MyComponent.tsx
type Props = {
prop1: string;
prop2: number;
};
const MyComponent = ({ prop1, prop2 }: Props): JSX.Element => {
return <div>{prop1} - {prop2}</div>;
};
export default MyComponent;- Import and use:
import MyComponent from '../components/MyComponent';
<MyComponent prop1="value" prop2={123} />Add new endpoints in src/services/api.ts:
// src/services/api.ts
export const api = {
myNewEndpoint: (param: string) =>
request(`/my-endpoint/${param}`, {
method: "POST",
body: JSON.stringify({ data: "value" }),
}),
};Use in components:
import { api } from "../services/api";
async function handleAction(): Promise<void> {
try {
const result = await api.myNewEndpoint("param");
console.log(result);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error(error);
}
}
}Main app layout with sidebar navigation.
Props:
children- Page content
Usage:
<Layout>
<Scanner />
</Layout>Generates forms from plugin schema metadata.
Props:
schema- Plugin schema objectpreset- Default preset ID (optional)onSubmit- Submit handler(data) => voidloading- Disable form during submission
Usage:
<DynamicForm
schema={pluginSchema}
onSubmit={(data) => console.log(data)}
loading={false}
/>Output Format:
{
preset: "quick",
inputs: {
target: "192.168.1.1",
port: 80,
verbose: true
}
}Confirmation dialog for scan consent.
Props:
plugin- Plugin objectonConfirm- Confirm handleronCancel- Cancel handler
Usage:
<ConsentModal
plugin={selectedPlugin}
onConfirm={handleStartScan}
onCancel={() => setShowModal(false)}
/>Display task information in a card.
Props:
task- Task object
Usage:
<TaskCard task={taskData} />Task Object:
{
task_id: "abc123",
plugin_name: "Nmap",
preset: "quick",
status: "running",
created_at: "2025-10-29T12:00:00Z",
finished_at: null
}Global state provider for plugins and settings.
Values:
{
plugins: [], // Array of plugin objects
settings: {}, // System settings
loading: false, // Initial load state
error: null, // Error message
reload: () => {} // Reload function
}Usage:
import { useApp } from '../context/AppContext'
function MyComponent() {
const { plugins, settings, loading } = useApp()
if (loading) return <div>Loading...</div>
return <div>{plugins.length} plugins</div>
}Layout:
.app- Main app container (grid).sidebar- Sidebar navigation.main- Main content area
Components:
.card- Content card with border.btn- Primary button.list- Unstyled list.log- Terminal-style output
Forms:
label- Form labelinput, select- Form inputs
Utilities:
.text-sm- Small text (14px).text-xs- Extra small (12px).text-muted- Muted color.mt-{1,2,3}- Margin top.mb-{1,2,3}- Margin bottom.flex- Flexbox container.grid- Grid container
.status-pending { background: #fef3c7; }
.status-running { background: #dbeafe; }
.status-completed { background: #d1fae5; }
.status-failed { background: #fee2e2; }
.status-cancelled { background: #e5e7eb; }All commands must be run from the frontend/ directory.
| Command | Description |
|---|---|
npm run dev |
Start the Vite development server |
npm run build |
Compile TypeScript and build for production |
npm run preview |
Preview the production build locally (port 8080) |
npm run typecheck |
Run TypeScript type checking without emitting files |
npm run test |
Run unit tests with Vitest |
npm run test:watch |
Run unit tests in watch mode (re-runs on file changes) |
npm run quality |
Run the quality gate checks |
npm run quality:full |
Run quality checks, typecheck, and tests together |
npm run e2e |
Run end-to-end tests with Playwright |
npm run e2e:ui |
Run end-to-end tests with Playwright's interactive UI |
- Can load plugin list from backend
- Can select a plugin and see its form
- Form fields match plugin schema
- Preset selection updates form defaults
- Consent modal appears before scan
- Task starts successfully
- Task details page updates in real-time
- Task history shows all tasks
- Can filter tasks by status
- Settings page loads correctly
- Navigation works between all pages
- Responsive design works on mobile
Run all frontend commands from the frontend/ directory.
cd frontend
npm installnpm run testnpm run test:watchnpm run typechecknpm run buildnpm run qualitynpm run quality:fullnpm run e2eVitest unit tests are located in:
frontend/testing/unitSupported naming patterns include:
*.test.ts *.test.tsx *.spec.ts *.spec.tsx
Note for Windows users: Some npm scripts using
NODE_OPTIONS=...may not run directly in PowerShell.
Run tests manually using:
npx vitest run- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
Error: Network error or Failed to fetch
Solution:
- Verify backend is running:
curl http://127.0.0.1:8000/api/v1/health - Check Vite proxy in
vite.config.ts:
server: {
proxy: {
'/api': 'http://127.0.0.1:8000'
}
}Error: Cannot read property 'plugins' of null
Solution:
- Check AppContext initialization
- Verify
/api/v1/pluginsendpoint returns data - Check browser console for API errors
Error: Task not starting
Solution:
- Check all required fields are filled
- Verify preset is selected
- Check browser console for validation errors
- Confirm consent modal was accepted
cd frontend
npm ci
npm run buildOutput lands in frontend/dist/. Verify locally before deploying:
npm run preview
# Serves dist/ at http://127.0.0.1:8080The frontend resolves the backend URL via src/api.ts::resolveApiBase() in this priority order:
| Priority | Mechanism | When to use |
|---|---|---|
| 1 | VITE_API_BASE env var |
Production — always set this |
| 2 | Window location heuristic | Dev server on a non-5173 port |
| 3 | Vite proxy (/api → backend) |
Local dev on port 5173 (default) |
VITE_API_BASE must be set at build time, not at runtime. Vite inlines import.meta.env values during the
build step — changing the env var after building has no effect.
VITE_API_BASE=http://your-backend-host:8081/api/v1 npm run buildOr create frontend/.env.production:
VITE_API_BASE=http://your-backend-host:8081/api/v1
⚠️ Do not confuseVITE_API_BASE(frontend, build-time) withVITE_API_PROXY_TARGET(Vite dev server only — has no effect in a production build).
⚠️ backend/secuscan/main.pycurrently importsStaticFilesbut does not mount thedist/directory. The backend cannot serve the frontend yet. Use one of the options below until that is wired up.
Option A — nginx (recommended)
server {
listen 80;
root /path/to/frontend/dist;
index index.html;
# SPA fallback — required for React Router
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to the backend
location /api/ {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
}
}Option B — quick local verification
npx serve dist --single
# --single enables the SPA fallback for React RouterSecuScan uses React Router for client-side navigation. If a user visits /task/abc123 directly or refreshes the page on any route, the web
server looks for a real file at that path, finds nothing, and returns a 404.
The fix is always the same: serve index.html for any path that does not match a real static file. The nginx try_files directive and
serve --single flag above both handle this. Without it, every direct link and browser refresh on a non-root route breaks.
The current docker-compose.yml runs the frontend service with npm run dev (Vite development server). This is intentional for
contributor workflows and is not production-ready. A multi-stage frontend Dockerfile is not yet present in the repository.
404 on page refresh or direct URL
The SPA fallback is missing. Add try_files $uri /index.html to your nginx config or use npx serve dist --single.
All API calls fail after deploying
VITE_API_BASE was not set at build time. Rebuild with the correct value:
VITE_API_BASE=http://your-backend:8081/api/v1 npm run buildVITE_API_PROXY_TARGET has no effect in production
This variable only configures the Vite dev server proxy. It is completely ignored in the production build.
CORS errors in the browser console
Add your frontend's origin to the backend config in .env:
SECUSCAN_CORS_ALLOWED_ORIGINS=http://your-frontend-host
- API Proxy: Vite dev server proxies
/apito backend - No Secrets: Frontend code is public - no API keys
- CORS: Backend must allow your frontend origin in dev (default includes
localhost:5173andlocalhost:3000) - Localhost Only: Both frontend and backend run locally
- Dark mode toggle
- Export task results (CSV/PDF)
- Real-time SSE streaming for live output
- WebSocket support for faster updates
- Toast notifications for actions
- Keyboard shortcuts
- Search/filter plugins
- Task comparison view
- Custom plugin upload
MIT License - Same as SecuScan project
For issues or questions:
- Check backend is running correctly
- Review browser console for errors
- Verify API responses with curl/Postman
- Check this README for common solutions
Last Updated: October 29, 2025 Version: 0.1.0-alpha