Frontend IP Protection¶
The dashboard frontend is a React SPA delivered to the browser. Any code in the JS bundle is visible to anyone with DevTools. This layer minimizes the proprietary information embedded in frontend code.
Dynamic Skill Catalog¶
Problem¶
The LaunchPentestDialog.tsx component previously hardcoded the entire skill catalog -- all 42 skills with names, groups, and parameter configurations -- directly in the JS bundle:
// BEFORE: Hardcoded in JS bundle (visible in DevTools → Sources)
const SKILL_OPTIONS = [
{ value: '/pentest', label: 'Full Pentest (/pentest)', group: 'Full' },
{ value: '/test-injection', label: 'Injection (/test-injection)', group: 'Testing' },
{ value: '/test-auth', label: 'Authentication (/test-auth)', group: 'Testing' },
// ... 39 more skills with full names and groupings
]
A competitor opening DevTools could see the complete list of proprietary skill names, how they're organized, and which groups they belong to.
Solution¶
The skill catalog is now loaded from a backend API endpoint that requires authentication:
The endpoint returns the full catalog only to authenticated admin/pentester users. The JS bundle contains only a minimal fallback:
// AFTER: Only fallback in JS bundle
const FALLBACK_SKILLS = [
{ value: '/pentest', label: 'Full Pentest', group: 'Full' },
]
Data Flow¶
sequenceDiagram
participant Browser
participant API as Backend API
participant Catalog as Skill Catalog
Browser->>API: GET /api/v1/skills/catalog<br/>Authorization: Bearer eyJ...
API->>API: Verify admin/pentester role
API->>Catalog: Load catalog
Catalog-->>API: 42 skills + 5 groups
API-->>Browser: { skills: [...], groups: [...] }
Browser->>Browser: Populate skill dropdown
Implementation¶
Backend (dashboard/backend/app/routers/skills.py):
- New router with
prefix="/skills"andrequire_role("admin", "pentester")dependency GET /catalogreturns{ skills: [...], groups: [...] }- Catalog defined server-side, never in frontend code
Frontend (dashboard/frontend/src/api/runner.ts):
- New
useSkillCatalog()hook using React Query - 5-minute cache (
staleTime: 5 * 60_000) to avoid redundant requests - Returns
SkillCatalogtype:{ skills: SkillOption[], groups: string[] }
Frontend (LaunchPentestDialog.tsx):
- Uses
useSkillCatalog()data when available - Falls back to
FALLBACK_SKILLS(single/pentestentry) while loading
Production Build Obfuscation¶
The production build applies two layers of obfuscation:
vite-plugin-obfuscator-- Structural obfuscation (control flow flattening, string encoding, dead code injection)- Terser -- Minification and mangling (variable names, console removal, comment stripping)
Layer 1: JavaScript Obfuscator (vite-plugin-obfuscator)¶
Based on javascript-obfuscator, this plugin transforms the code structure to make reverse engineering significantly harder. It only runs in production builds (mode === 'production').
// vite.config.ts — production only
obfuscatorPlugin({
options: {
controlFlowFlattening: true, // Flatten if/else into switch tables
controlFlowFlatteningThreshold: 0.5, // Apply to 50% of blocks
deadCodeInjection: true, // Inject meaningless code paths
deadCodeInjectionThreshold: 0.2, // 20% dead code ratio
stringArray: true, // Move strings to encoded array
stringArrayEncoding: ['base64'], // Base64-encode string literals
stringArrayThreshold: 0.5, // Encode 50% of strings
selfDefending: true, // Break if code is reformatted
identifierNamesGenerator: 'hexadecimal', // _0x1a2b3c style names
compact: true, // Single-line output
disableConsoleOutput: true, // Remove console.*
},
})
| Technique | What It Does | Impact on RE |
|---|---|---|
| Control flow flattening | Converts if/else/for into opaque switch tables |
Makes logic flow impossible to follow linearly |
| Dead code injection | Adds unreachable code blocks | Analyst wastes time on fake logic |
| String array encoding | Moves "api/v1/skills" into _0x4a2b[23] |
Hides endpoint URLs and error messages |
| Self-defending | Code breaks if pretty-printed | Prevents casual DevTools inspection |
| Hexadecimal identifiers | handleSubmit becomes _0x1f3a2c |
No semantic information in names |
Layer 2: Terser Minification¶
Applied after obfuscation as a second pass:
// vite.config.ts
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove all console.log() calls
drop_debugger: true, // Remove debugger statements
passes: 2, // Two-pass compression
},
mangle: {
toplevel: true, // Mangle top-level variable names
properties: {
regex: /^_/, // Mangle underscore-prefixed properties
},
},
format: {
comments: false, // Strip all comments
},
},
}
Combined Effect¶
| Before (Development) | After (Production) |
|---|---|
const SKILL_OPTIONS = [{ value: '/pentest', label: 'Full Pentest' }] |
Not present (loaded from API) |
function handleSubmit(target, skill) { ... } |
function _0x1f3a(_0x2b,_0x4c){switch(_0x2b){case 0x3:...}} |
console.log('Starting session:', sessionId) |
Removed entirely |
// Validate credentials before launch |
Removed entirely |
if (mode === 'stealth') { rate = 2 } |
Flattened into opaque switch table |
Performance Impact¶
Build time vs. bundle size
Obfuscation increases build time by ~30-60 seconds but the output bundle size is comparable to non-obfuscated (dead code injection adds ~5-10% but Terser compression offsets this). Runtime performance is unaffected -- the obfuscated code executes identically.
Limitations¶
Obfuscation is not encryption
Obfuscation raises the bar significantly but is not unbreakable. A determined attacker with time and a JavaScript deobfuscator (e.g., synchrony, js-deobfuscator) can partially reverse the transformations. The real protection is keeping sensitive logic server-side (which the skill catalog migration achieves). Obfuscation is a deterrent, not a guarantee.
Removed: raw Type Field¶
The EngagementDetail TypeScript interface previously included a raw field:
// BEFORE
export interface EngagementDetail {
// ...
raw?: Record<string, unknown> // Full context.json
}
// AFTER
export interface EngagementDetail {
// ...
// raw field removed — not sent by backend
}
This ensures the frontend doesn't expect or process the raw context.json data, which is no longer sent by the backend.
What Remains Visible¶
Even with these protections, some information is inherently visible in the frontend:
| Visible | Why It's Acceptable |
|---|---|
| Phase names (recon, discover, scan, test, verify, report) | Generic methodology phases, not proprietary |
| Severity levels (Critical, High, Medium, Low, Info) | Industry standard (CVSS) |
| CVSS scoring thresholds | Published CVSS 4.0 specification |
| Tech stack field names (backend, frontend, WAF) | Common fingerprinting categories |
| Auth type indicators (JWT, OAuth, SAML) | Standard authentication protocols |
| UI layout and component structure | Visual design, not methodology |
The key IP -- how tests are executed, which techniques are applied, and what decision logic drives the AI -- remains entirely server-side.