Skip to content

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:

GET /api/v1/skills/catalog
Authorization: Bearer <admin_or_pentester_token>

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" and require_role("admin", "pentester") dependency
  • GET /catalog returns { 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 SkillCatalog type: { skills: SkillOption[], groups: string[] }

Frontend (LaunchPentestDialog.tsx):

  • Uses useSkillCatalog() data when available
  • Falls back to FALLBACK_SKILLS (single /pentest entry) while loading

Production Build Obfuscation

The production build applies two layers of obfuscation:

  1. vite-plugin-obfuscator -- Structural obfuscation (control flow flattening, string encoding, dead code injection)
  2. 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.