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(notRequest) for routes behindauthenticate - Use
requirePermission(PERMISSIONS.SOME_PERMISSION)for admin-only routes - Use
getTenantAwareRepository(Entity)instead ofAppDataSource.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: falseon sensitive columns (likepasswordHashin the User entity) - Use
@BeforeInsert/@BeforeUpdatefor 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:
| Script | Purpose | File |
|---|---|---|
migrate:local | Local dev (public schema, synchronize + seed) | backend/src/scripts/migrate-local.ts |
migrate:dev | Cloud DB, multi-tenant aware | backend/src/scripts/migrate-dev.ts |
migrate:production | Transaction-safe, validated | backend/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:
- Sidebar
href-- the link the user clicks - App.tsx
path-- the route definition - 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
BaseAttackAgentand implementsrun() - 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
-
agentNamematches the DB seednamefield - 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.