Skip to main content
Version: Local Β· In Progress

Adding Features to ThreatWeaver

This guide walks through the two most common feature-addition workflows: creating a new REST API endpoint (full-stack) and adding a new scanner agent to the AppSec module.


How to Add a New API Endpoint​

Adding a feature end-to-end involves backend route + service + entity, then frontend API client + page + routing. Follow these ten steps in order.

Step 1 -- Create the Route File​

Create a new file in backend/src/routes/. Follow the naming convention <feature>.routes.ts.

Every route file exports a Router and applies the authenticate middleware at the top. Here is the standard skeleton derived from the actual codebase pattern used in admin.routes.ts:

// backend/src/routes/myFeature.routes.ts
import { Router, Request, Response } from 'express'
import { authenticate, requirePermission, AuthenticatedRequest } from '../middleware/auth.js'
import { PERMISSIONS } from '../entities/index.js'
import { getTenantAwareRepository } from '../multi-tenant/tenant-local-storage.js'

const router = Router()

// All routes require authentication
router.use(authenticate)

// GET /api/my-feature
router.get('/', async (req: AuthenticatedRequest, res: Response) => {
try {
// Your logic here
res.json({ data: [] })
} catch (err: any) {
res.status(500).json({ error: err.message })
}
})

export default router

Key patterns from the codebase:

  • Use AuthenticatedRequest (not Request) for routes behind authenticate
  • Use requirePermission(PERMISSIONS.SOME_PERMISSION) for admin-only routes
  • Use getTenantAwareRepository(Entity) instead of AppDataSource.getRepository(Entity) for multi-tenant safety
  • Always wrap handler bodies in try/catch

Step 2 -- Register the Route in backend/src/index.ts​

Open backend/src/index.ts and add your route import at the top alongside the other route imports:

import myFeatureRoutes from './routes/myFeature.routes.js'

Then register it with the Express app in the route-mounting section (around line 440+):

app.use('/api/my-feature', myFeatureRoutes)

If your route requires the authenticate middleware to be applied globally (like /api/scans), add it inline:

app.use('/api/my-feature', authenticate, myFeatureRoutes)

Where to place it: Look at the existing route registrations in index.ts. Routes are grouped by module. Place yours near related functionality.

Step 3 -- Create a Service​

Create a service file in backend/src/services/. Services contain business logic and are imported by routes. Follow the singleton pattern used throughout the codebase:

// backend/src/services/myFeature.service.ts
import { getTenantAwareRepository } from '../multi-tenant/tenant-local-storage.js'
import { MyEntity } from '../entities/index.js'

class MyFeatureService {
async getAll(): Promise<MyEntity[]> {
const repo = getTenantAwareRepository(MyEntity)
return repo.find()
}

async getById(id: string): Promise<MyEntity | null> {
const repo = getTenantAwareRepository(MyEntity)
return repo.findOne({ where: { id } })
}

async create(data: Partial<MyEntity>): Promise<MyEntity> {
const repo = getTenantAwareRepository(MyEntity)
const entity = repo.create(data)
return repo.save(entity)
}
}

// Singleton export -- matches project convention
export const myFeatureService = new MyFeatureService()

Important: Always use getTenantAwareRepository() from ../multi-tenant/tenant-local-storage.js. This ensures your queries hit the correct tenant schema in multi-tenant deployments and falls back to AppDataSource in single-tenant mode.

Step 4 -- Create an Entity (if needed)​

Create a new entity file in backend/src/entities/. Here is the pattern from User.entity.ts:

// backend/src/entities/MyEntity.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
} from 'typeorm'

@Entity('my_entities')
export class MyEntity {
@PrimaryGeneratedColumn('uuid')
id!: string

@Column({ type: 'varchar', length: 255 })
name!: string

@Column({ type: 'text', nullable: true })
description!: string | null

@Column({ type: 'varchar', length: 50, default: 'active' })
status!: string

@CreateDateColumn()
createdAt!: Date

@UpdateDateColumn()
updatedAt!: Date
}

Then re-export it from the barrel file backend/src/entities/index.ts:

export { MyEntity } from './MyEntity.entity.js'

Key conventions from the codebase:

  • Use PrimaryGeneratedColumn('uuid') for IDs
  • Use ! (definite assignment) on all entity fields
  • Use select: false on sensitive columns (like passwordHash in the User entity)
  • Use @BeforeInsert / @BeforeUpdate for lifecycle hooks (e.g., password hashing)
  • Table names use snake_case plural (my_entities)

Step 5 -- Create a Migration​

ThreatWeaver uses three migration scripts. You must update all three:

ScriptPurposeFile
migrate:localLocal dev (public schema, synchronize + seed)backend/src/scripts/migrate-local.ts
migrate:devCloud DB, multi-tenant awarebackend/src/scripts/migrate-dev.ts
migrate:productionTransaction-safe, validatedbackend/src/scripts/migrate-production.ts

Add your DDL to each migration script. Run locally first:

cd backend && npm run migrate:local

Check migration status:

cd backend && npm run migrate:status

Step 6 -- Add Tests​

Create a test file in backend/src/__tests__/ or alongside your service:

// backend/src/__tests__/myFeature.test.ts
import { describe, it, expect, vi } from 'vitest'
import { myFeatureService } from '../services/myFeature.service.js'

describe('MyFeatureService', () => {
it('should return all items', async () => {
const items = await myFeatureService.getAll()
expect(Array.isArray(items)).toBe(true)
})
})

Run tests:

cd backend && npm test

Step 7 -- Create Frontend API Client Function​

Add your API call to frontend/src/services/api.ts (or a dedicated service file). The project uses a centralized API client with interceptors for auth tokens:

// In frontend/src/services/api.ts or a new service file
export const myFeatureApi = {
getAll: () => api.get('/my-feature'),
getById: (id: string) => api.get(`/my-feature/${id}`),
create: (data: any) => api.post('/my-feature', data),
update: (id: string, data: any) => api.put(`/my-feature/${id}`, data),
delete: (id: string) => api.delete(`/my-feature/${id}`),
}

Step 8 -- Create a Page Component​

Create your page in frontend/src/pages/:

// frontend/src/pages/MyFeaturePage.tsx
import React, { useEffect, useState } from 'react'
import { myFeatureApi } from '../services/api'

export default function MyFeaturePage() {
const [data, setData] = useState<any[]>([])
const [loading, setLoading] = useState(true)

useEffect(() => {
myFeatureApi.getAll()
.then(res => setData(res.data))
.catch(console.error)
.finally(() => setLoading(false))
}, [])

if (loading) return <div>Loading...</div>

return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">My Feature</h1>
{/* Render your data */}
</div>
)
}

Step 9 -- Add Route in frontend/src/App.tsx​

Add a <Route> entry inside the router configuration:

import MyFeaturePage from './pages/MyFeaturePage'

// Inside the router/switch:
<Route path="/my-feature" element={<MyFeaturePage />} />

Step 10 -- Add to Sidebar​

Open frontend/src/components/Sidebar.tsx and add a navigation item. All three must match for the routing to work:

  1. Sidebar href -- the link the user clicks
  2. App.tsx path -- the route definition
  3. Component import -- the actual page component
// In the sidebar navigation items array:
{
name: 'My Feature',
href: '/my-feature',
icon: SomeIcon,
}

Full-Stack Checklist​

  • Route file created in backend/src/routes/
  • Route registered in backend/src/index.ts
  • Service created in backend/src/services/
  • Entity created in backend/src/entities/ (if needed)
  • Entity re-exported from backend/src/entities/index.ts
  • All 3 migration scripts updated
  • Tests added and passing
  • Frontend API client function added
  • Page component created in frontend/src/pages/
  • Route added in frontend/src/App.tsx
  • Sidebar entry added in frontend/src/components/Sidebar.tsx
  • Backend builds: cd backend && npm run build
  • Frontend builds: cd frontend && npm run build

How to Add a New Scanner Agent​

The AppSec scanner uses a multi-agent architecture. Each agent is a focused class that tests one category of vulnerability. As of the latest codebase, there are 56+ agents.

Step 1 -- Understand the Base Class​

All agents extend BaseAttackAgent defined in backend/src/services/appsec/agents/baseAttackAgent.ts. The base class provides:

  • Standard lifecycle: init(), run(), cleanup()
  • Scope enforcement helpers
  • Rate limiting and timeout tracking
  • Request counting for resource limits (budget system)
  • KnowledgeBase feedback loop: recordProbeResult(), getRecommendedPayloads()
  • Adaptive attack loop: observeEndpoint(), classifyResponse(), selectPayloads(), analyzeProbeResult(), shouldSkipParameter()

The key interface your agent returns is AgentFinding:

export interface AgentFinding {
type: string // 'xss' | 'sqli' | 'ssrf' | etc.
severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
title: string
description: string
endpoint: string
method: string
parameter?: string
payload?: string
evidence: {
request: string
response: string
proofType: 'reflection' | 'differential' | 'timing' | 'callback' | 'deterministic'
}
confidence: number // 0.0 to 1.0
cweId?: string
owaspCategory?: string
remediation?: string
}

Step 2 -- Create the Agent File​

Create a new file in backend/src/services/appsec/agents/. Follow the naming convention <agentName>.agent.ts:

// backend/src/services/appsec/agents/myVulnHunter.agent.ts
import { BaseAttackAgent, AgentFinding } from './baseAttackAgent.js'
import type { EndpointObservation, SharedBlackboard } from '../sharedBlackboard.service.js'
import type { PentestAiAdapter } from '../pentestAiAdapter.service.js'
import type { ScannerPolicy } from '../scannerConstants.js'

class MyVulnHunterAgent extends BaseAttackAgent {
constructor() {
super()
this.agentName = 'my_vuln_hunter'
}

/**
* Main attack method -- called by the coordinator.
* Returns an array of findings discovered during the scan.
*/
async run(
blackboard: SharedBlackboard,
aiAdapter: PentestAiAdapter,
policy: ScannerPolicy
): Promise<AgentFinding[]> {
const findings: AgentFinding[] = []
const endpoints = blackboard.getEndpoints()

for (const endpoint of endpoints) {
if (this.isOverBudget()) break

// Your vulnerability testing logic here
// Use this.probe() for HTTP requests (respects rate limits)
// Use blackboard.getParams(endpoint) for discovered parameters
// Use aiAdapter for AI-assisted analysis

// Example finding:
// findings.push({
// type: 'my_vuln_type',
// severity: 'high',
// title: 'Vulnerability Found',
// description: 'Detailed description...',
// endpoint: endpoint.url,
// method: endpoint.method,
// evidence: { request: '...', response: '...', proofType: 'deterministic' },
// confidence: 0.9,
// cweId: 'CWE-XXX',
// })
}

return findings
}
}

// Singleton export
export const myVulnHunter = new MyVulnHunterAgent()

Step 3 -- Register in pentestCoordinator.service.ts​

Open backend/src/services/appsec/pentestCoordinator.service.ts and add two things:

1. Import the agent (at the top, with the other agent imports around lines 17-75):

import { myVulnHunter } from './agents/myVulnHunter.agent.js'

2. Add to the agent registry in the coordinator's agent array. The coordinator runs agents in parallel during Phase 3 (attacking), respecting execution-phase ordering and AI-generated plan reordering.

Step 4 -- Add to Migration Seeder​

Each agent needs a corresponding record in the PentestAgentConfig table. Add a seed entry in all three migration scripts:

INSERT INTO pentest_agent_configs (id, name, display_name, category, enabled, execution_phase)
VALUES (
gen_random_uuid(),
'my_vuln_hunter',
'My Vuln Hunter',
'injection', -- category: injection | auth | config | info | logic
true,
3 -- execution phase (1-5)
);

The name field must exactly match this.agentName in your agent class.

Agent Development Checklist​

  • Agent file created in backend/src/services/appsec/agents/
  • Extends BaseAttackAgent and implements run()
  • Returns properly structured AgentFinding[]
  • Singleton exported
  • Imported in pentestCoordinator.service.ts
  • Added to agent registry / array in coordinator
  • Seed record added to all 3 migration scripts
  • agentName matches the DB seed name field
  • Backend builds: cd backend && npm run build
  • Test with a local scan against a target

Agent Architecture at a Glance​

The coordinator orchestrates the full scan pipeline:

Phase 0: Bootstrap (auth, target validation)
Phase 1: Discovery (endpoint enumeration, crawling)
Phase 2: Profiling (parameter analysis, technology detection)
Phase 3: Attacking (56+ agents run in parallel, budget-managed)
Phase 4: Validation (AI-powered finding verification, deduplication)
Phase 5: Chaining (exploit chain analysis, report generation)

Each agent in Phase 3 receives a SharedBlackboard containing all discovered endpoints and parameters from Phases 1-2, an PentestAiAdapter for AI-assisted analysis, and a ScannerPolicy for scope enforcement.