Skip to content

samurai

Design system and architecture documentation for the Samurai cybersecurity platform. Use when building or modifying any UI component, backend endpoint, or feature module in Samurai.

projectv1.0.0skills/web/cybersec/samurai
View source on GitHub

Samurai Design System & Architecture

Samurai is a cybersecurity analysis platform built with an Angular 21 frontend and FastAPI backend. The design follows the Samurai Design System philosophy: monochromatic, typographically driven, information-dense without clutter.


1. DESIGN PHILOSOPHY

  • Subtract, don't add. Every element must earn its pixel. Default to removal.
  • Structure is ornament. Expose the grid, the data, the hierarchy itself.
  • Monochrome is the canvas. Color is an event, not a default -- except when encoding data status.
  • Type does the heavy lifting. Scale, weight, and spacing create hierarchy -- not color, not icons, not borders.
  • Both modes are first-class. Dark mode: OLED black. Light mode: warm off-white.
  • Industrial warmth. Technical and precise, but never cold. A human hand should be felt.

2. FONT DISCIPLINE

Per screen, use maximum:

  • 2 font families (Space Grotesk + Space Mono. Doto only for hero moments.)
  • 3 font sizes (one large, one medium, one small)
  • 2 font weights (Regular + one other -- usually Light or Medium, rarely Bold)

Font Assignment (Fixed Rules):

| Context | Font | Size | Weight | Letter-Spacing | |---------|------|------|--------|----------------| | Hero numbers, display | Doto | 48px–72px | Variable | -0.03em | | Headings | Space Grotesk | 24px | Regular | -0.01em | | Body text | Space Grotesk | 16px | Light/Regular | 0 | | UI Labels | Space Mono | 11px | Regular | 0.08em ALL CAPS | | Data/Numbers | Space Mono | 14–16px | Regular | 0 | | Terminal/logs | Space Mono | 13px | Regular | 0 |


3. FEATURE-DRIVEN ARCHITECTURE

See references/architecture-overview.md for the full system architecture.

The frontend follows a strict feature-driven structure:

frontend/src/app/
├── app.component.ts        # Root shell: sidebar nav + router-outlet
├── app.routes.ts           # Lazy-loaded routes (4 features)
├── app.config.ts           # HttpClient + Router providers
├── services/
│   └── theme.service.ts    # Dark/Light mode with Angular Signals
└── features/
    ├── scanner/            # Nmap port scanning
    ├── recon/              # Web reconnaissance
    ├── vulnerabilities/    # DAST vulnerability crawling
    └── history/            # Scan history & archive

Each feature is self-contained with its own components, models, and services.


4. COMPONENT PATTERNS

See references/component-patterns.md for detailed component specifications.

Key patterns to follow:

  1. All components are standalone (Angular 21, no NgModules).
  2. Input/Output only -- no service injection in reusable components. Use @Input() for data, @Output() EventEmitter for actions.
  3. State lives in parent -- feature components hold state, child components are pure rendering.
  4. ChangeDetectorRef.detectChanges() for WebSocket-driven updates.
  5. No global state library -- state is managed locally per feature.

5. BACKEND PATTERNS

See references/backend-patterns.md for API and service architecture.

  • WebSocket-first: All long-running operations (scanning, crawling, recon) use WebSockets for real-time streaming.
  • REST for CRUD: Short-lived operations (list, get, delete) use standard REST.
  • DB Dependency Injection: All endpoints use db: Session = Depends(get_db).
  • Subprocess Integration: External tools (Nmap, SQLMap, Nuclei) run as OS subprocesses via asyncio.create_subprocess_exec.
  • Module Orchestration: Recon modules are pluggable async functions conforming to a ModuleRunner callable type.

6. DATABASE SCHEMA

See references/database-schema.md for complete schema documentation.

Three tables:

  • scans -- Scan records (target, status, type, timestamp)
  • discovered_links -- URLs discovered during crawling (FK → scans)
  • findings -- Security findings/vulnerabilities (FK → scans, FK → links)

With cascade delete relationships: deleting a scan removes all its links and findings.


7. EXPORT SYSTEM

See references/export-patterns.md for full export architecture.

Exports are implemented client-side using:

  • CSV: Manual string building with CSV escaping
  • JSON: JSON.stringify(payload, null, 2)
  • PDF: jsPDF + jspdf-autotable, landscape A4, dark-themed
  • Binary: pako gzip compression of JSON payload

Each feature has its own export-actions component with 4 buttons: CSV, JSON, PDF, BIN.


8. ANTI-PATTERNS -- WHAT TO NEVER DO

  • No gradients in UI chrome
  • No shadows. No blur. Flat surfaces, border separation.
  • No skeleton loading screens. Use [LOADING...] text.
  • No toast popups. Use inline status text: [SAVED], [ERROR: ...]
  • No sad-face illustrations, cute mascots, or multi-paragraph empty states
  • No zebra striping in tables
  • No filled icons, multi-color icons, or emoji as UI
  • No parallax, scroll-jacking, or gratuitous animation
  • No spring/bounce easing. Use subtle ease-out only.
  • No border-radius > 16px on cards. Buttons are pill (999px) or technical (4-8px).

9. REFERENCE FILES

For detailed specifications:

  • references/architecture-overview.md -- System architecture, Docker services, API endpoints, tech stack
  • references/design-tokens.md -- CSS custom properties, type scale, color system (dark + light), spacing, motion
  • references/component-patterns.md -- Component structure, button variants, inputs, tables, navigation, export actions
  • references/backend-patterns.md -- API routes, WebSocket endpoints, service layer, database interaction
  • references/database-schema.md -- ORM models, relationships, cascade rules, query patterns
  • references/export-patterns.md -- Client-side export architecture, format implementations, encryption patterns

References (6)

architecture-overview

Samurai Design System -- Architecture Overview

This documents the complete system architecture of the Samurai cybersecurity platform.


1. SYSTEM DIAGRAM

┌─────────────────────────────────────────────────────────────────┐
│                       DOCKER COMPOSE                            │
│  ┌───────────────┐  ┌───────────────┐  ┌───────┐  ┌─────────┐  │
│  │   FRONTEND    │  │    BACKEND    │  │ REDIS │  │   DB    │  │
│  │  Angular 21   │  │   FastAPI     │  │alpine │  │ PG 15   │  │
│  │   :4200       │──│   :8000       │  │:6379  │  │ :5432   │  │
│  │   ng serve    │  │   uvicorn     │──│        │──│         │  │
│  │   (HMR)       │  │   (reload)    │  │        │  │         │  │
│  └───────────────┘  └───────────────┘  └───────┘  └─────────┘  │
│         │                    │                                   │
│         └──── WebSocket ────┘ (3 WS endpoints)                  │
│              + REST API (5 HTTP endpoints)                       │
└─────────────────────────────────────────────────────────────────┘

2. TECH STACK

Frontend

| Technology | Version | Purpose | |---|---|---| | Angular (standalone) | 21.2.8 | SPA framework | | TypeScript | 5.9.3 | Language | | SCSS | - | Styling | | RxJS | 7.8.1 | Reactive streams | | jsPDF + jspdf-autotable | 4.2.1 / 5.0.7 | PDF export generation | | pako | 2.1.0 | gzip for binary exports |

Backend

| Technology | Version | Purpose | |---|---|---| | FastAPI | 0.105.0 | REST + WebSocket API framework | | Uvicorn | 0.24.0 | ASGI server | | SQLAlchemy | 2.0.23 | ORM + session management | | PostgreSQL | 15-alpine | Primary database | | cryptography | 42.0.2 | AES encryption (available) | | Playwright | 1.52.0 | Headless browser analysis | | httpx | 0.25.2 | Async HTTP client | | requests | 2.31.0 | Sync HTTP for probes | | BeautifulSoup4 | 4.12.2 | HTML parsing | | dnspython | 2.5.0 | DNS resolution |

Infrastructure

| Component | Technology | |---|---| | Containerization | Docker Compose (4 services) | | Dev mode | ng serve + HMR, uvicorn --reload | | External tools | Nmap, SQLMap, Nuclei (in container) |


3. API ENDPOINTS

REST Endpoints

| Method | Path | Description | |---|---|---| | GET | / | Health check | | GET | /api/scans | List all scans (ordered by ID desc) | | GET | /api/scans/{scan_id} | Get scan with findings + links (eager loaded) | | DELETE | /api/scans/{scan_id} | Delete scan + cascade | | POST | /api/scan/cancel/{scan_id} | Cancel running scan | | GET | /api/database/export/raw | Download full database as JSON | | POST | /api/database/export/encrypted | Download encrypted database dump |

WebSocket Endpoints

| Path | Parameters | Engine | |---|---|---| | /api/scan/live | target, profile, timeout, web_scan, collect_contacts, scan_unsanitized, max_pages | scanner.py - Nmap + web surface scan | | /api/vuln/live | target, modules, auth_mode, auth_bearer, auth_user, auth_pass, auth_cookie | crawler.py - DAST vulnerability crawler | | /api/recon/live | target, recon_types, timeout | recon/orchestrator.py - Web recon modules |


4. FRONTEND ROUTES

| Path | Component | Description | |---|---|---| | /scanner | ScannerComponent | Nmap port scanning dashboard | | /recon | ReconComponent | Web reconnaissance dashboard | | /vulnerabilities | VulnerabilitiesComponent | DAST vulnerability crawler | | /history | HistoryComponent | Scan history archive | | /export | ExportDatabaseComponent | Full database export | | / | Redirect → /scanner | Default route |

All routes use lazy loading via loadComponent.


5. DIRECTORY STRUCTURE

samurai/
├── docs/                              # Documentation
│   ├── manual.md                      # Dev & production setup
│   ├── python-libraries.md            # Backend dependency inventory
│   ├── ui-architecture.md             # UI architecture philosophy
│   ├── uses/                          # Use case docs
│   │   └── dast.md
│   └── skills/                        # AI-consumable design docs (THIS)
│       ├── SKILL.md
│       └── references/
│           ├── design-tokens.md
│           ├── component-patterns.md
│           ├── backend-patterns.md
│           ├── database-schema.md
│           ├── export-patterns.md
│           └── architecture-overview.md
├── backend/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── app/
│       ├── main.py                    # FastAPI app + all routes
│       ├── database.py                # SQLAlchemy engine + session
│       ├── models.py                  # ORM models
│       ├── scanner.py                 # Nmap + web surface scan engine
│       ├── crawler.py                 # DAST vulnerability crawler
│       ├── db_exporter.py             # Database export engine (NEW)
│       └── recon/                     # Web recon subsystem
│           ├── orchestrator.py
│           ├── target.py
│           ├── logger.py
│           ├── types.py
│           └── modules/
├── frontend/
│   ├── Dockerfile
│   ├── nginx.conf
│   ├── angular.json
│   ├── package.json
│   └── src/
│       ├── main.ts
│       ├── index.html
│       ├── styles.scss                # Samurai Design System tokens
│       └── app/
│           ├── app.component.ts/html/scss
│           ├── app.config.ts
│           ├── app.routes.ts
│           ├── services/
│           │   └── theme.service.ts
│           └── features/
│               ├── scanner/
│               ├── recon/
│               ├── vulnerabilities/
│               ├── history/
│               └── export-database/   # (NEW)
└── docker-compose.yml

6. DATA FLOW

Scanning Flow (WebSocket)

  1. User enters target + config → clicks "Start Scan"
  2. Frontend opens WebSocket to /api/{type}/live?target=...
  3. Backend creates Scan record → sends [SCAN_META] scan_id={id}
  4. Backend runs tool/analysis → streams stdout + findings over WebSocket
  5. Findings stored as Finding records linked to the Scan
  6. On completion: scan status = COMPLETED, [done] message sent

History/Replay Flow

  1. User navigates to /history
  2. Frontend GETs /api/scans → lists all scans
  3. Clicking a scan navigates to appropriate feature with scanId query param
  4. Feature page detects scanId → GETs /api/scans/{id} → rehydrates UI from DB

Export Flow (Current)

  1. Feature component collects all scan data into a payload object
  2. User clicks export button → feature calls format function
  3. JS creates Blob → creates download URL → triggers browser download
  4. All export logic is client-side, no backend involved

Database Export Flow (NEW)

  1. User navigates to /export
  2. Chooses direct export → backend queries all scans+findings+links → returns JSON file
  3. OR chooses encrypted export → enters password → backend encrypts with AES → returns .bin file
backend-patterns

Samurai Design System -- Backend Patterns

This documents the FastAPI backend architecture, conventions, and patterns used in Samurai.


1. API ENTRY POINT

File: backend/app/main.py

from fastapi import FastAPI, Depends, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session

app = FastAPI(title="Samurai API", version="2.5.0")

@app.on_event("startup")
def init_database():
    database.wait_for_db()
    models.Base.metadata.create_all(bind=database.engine)

app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)

Conventions

  • Version string: "2.5.0" (also in app.component.html footer and ROADMAP.md)
  • CORS: wide open for development
  • DB tables auto-created on startup (after waiting for DB availability)
  • All routes are defined directly in main.py

2. DATABASE CONNECTION

File: backend/app/database.py

from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker, declarative_base

DB_USER = os.getenv("DB_USER", "postgres")
DB_PASS = os.getenv("DB_PASS", "postgres")
DB_HOST = os.getenv("DB_HOST", "db")
DB_NAME = os.getenv("DB_NAME", "samurai")

DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}"

engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def wait_for_db(max_retries=30, retry_delay=1.5):
    # Retries connection until DB is available

Key Points

  • get_db() is the FastAPI dependency for session injection
  • wait_for_db() handles Docker startup ordering (DB may start after backend)
  • pool_pre_ping=True ensures stale connections are detected

3. ORM MODELS

File: backend/app/models.py

See database-schema.md for complete schema. Key patterns:

  • Scan is the root entity with cascade-delete relationships
  • Base is imported from database.py (not directly from SQLAlchemy)
  • Default status is "RUNNING"
  • created_at uses datetime.utcnow
  • poc_payload is unlimited-length String (formerly had a limit that was removed)

4. REST ENDPOINT PATTERNS

Standard CRUD endpoint:

@app.get("/api/resource")
def list_resource(db: Session = Depends(database.get_db)):
    items = db.query(models.Model).order_by(models.Model.id.desc()).all()
    return items

@app.get("/api/resource/{id}")
def get_resource(id: int, db: Session = Depends(database.get_db)):
    item = db.query(models.Model).filter(models.Model.id == id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Not found")
    return item

@app.delete("/api/resource/{id}")
def delete_resource(id: int, db: Session = Depends(database.get_db)):
    item = db.query(models.Model).filter(models.Model.id == id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Not found")
    db.delete(item)
    db.commit()
    return {"status": "deleted", "id": id}

Eager loading for relationships:

scan = db.query(models.Scan)\
    .options(
        joinedload(models.Scan.findings),
        joinedload(models.Scan.discovered_links)
            .joinedload(models.DiscoveredLink.findings)
    )\
    .filter(models.Scan.id == scan_id)\
    .first()

5. WEBSOCKET ENDPOINT PATTERNS

Standard WebSocket endpoint:

@app.websocket("/api/endpoint/live")
async def websocket_endpoint(
    websocket: WebSocket,
    target: str,
    param1: str = "default",
    db: Session = Depends(database.get_db)
):
    await websocket.accept()
    try:
        # Create scan record
        scan_record = models.Scan(
            domain_target=target,
            status="RUNNING",
            scan_type="type_identifier"
        )
        db.add(scan_record)
        db.commit()
        db.refresh(scan_record)

        await websocket.send_text(f"[SCAN_META] scan_id={scan_record.id}")

        # Perform the actual work
        await perform_operation(target, websocket, db, ...)

        # Mark complete
        scan_record.status = "COMPLETED"
        db.commit()
        await websocket.send_text("[done] scan completed and saved to history")

    except WebSocketDisconnect:
        if scan_record:
            scan_record.status = "CANCELLED"
            db.commit()
    except Exception as e:
        if scan_record:
            scan_record.status = "ERROR"
            db.commit()
        await websocket.send_text(f"[!] CRITICAL ERROR: {str(e)}")
        await websocket.close()

WebSocket Message Convention

  • [SCAN_META] scan_id={id} -- First message, identifies the scan record
  • [...] -- Bracket-prefixed log/status messages
  • [!] ... -- Error messages
  • [done] ... -- Completion message

6. ASYNCHRONOUS SERVICE LAYER

Long-running operations are async functions running in the WebSocket handler:

Subprocess Execution Pattern

process = await asyncio.create_subprocess_exec(
    "tool-name", *args,
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.STDOUT
)

async for line in process.stdout:
    decoded = line.decode("utf-8", errors="replace").rstrip()
    await websocket.send_text(decoded)

Sync HTTP in Async Context

Synchronous requests calls are wrapped with asyncio.to_thread():

response = await asyncio.to_thread(requests.get, url, timeout=10)

Module Orchestration

The recon system uses a pluggable module pattern:

# Each module is a callable:
async def run_module(target: str, results: dict, logger: ReconStreamLogger):
    ...

# Orchestrator runs them sequentially:
for module_name in requested_modules:
    await run_module(target, all_results, logger)

7. ADDING A NEW ENDPOINT

  1. If it's a long operation with streaming output → use WebSocket
  2. If it's a quick CRUD operation → use REST
  3. Add route in main.py following existing conventions
  4. Use db: Session = Depends(database.get_db) for DB access
  5. Follow error handling patterns (HTTPException for REST, try/except for WS)
  6. Use the [SCAN_META], [done], [!] message conventions

8. NEW FILE EXPORT (Database Dump)

The database export is a new POST endpoint pattern:

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64, os, json

@app.get("/api/database/export/raw")
def export_db_raw(db: Session = Depends(database.get_db)):
    # Query all data
    # Return StreamingResponse or FileResponse

@app.post("/api/database/export/encrypted")
def export_db_encrypted(payload: dict, db: Session = Depends(database.get_db)):
    password = payload.get("password", "")
    # Derive key from password
    # Encrypt database dump
    # Return encrypted file

9. ENVIRONMENT VARIABLES (Docker Compose)

environment:
  REDIS_URL: redis://redis:6379/0
  DB_HOST: db
  DB_NAME: samurai
  DB_USER: postgres
  DB_PASS: postgres

Access in Python with:

os.getenv("DB_HOST", "db")
component-patterns

Samurai Design System -- Component Patterns

This documents the actual component patterns used in the Samurai frontend, based on the Samurai design philosophy.


1. COMPONENT ARCHITECTURE RULES

1.1 Standalone Components (Mandatory)

All components are Angular standalone. No NgModules. Import what you need:

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './my.component.html',
  styleUrls: ['./my.component.scss']
})

1.2 Communication Pattern

  • Parent → Child: @Input() bindings
  • Child → Parent: @Output() EventEmitter
  • No global state: State lives in feature-level components
  • No service injection in reusable/dumb components

1.3 Template Rendering

Use *ngIf for conditional rendering. Use *ngFor with trackBy where possible.

1.4 Change Detection for WebSockets

For components receiving WebSocket data streams, inject ChangeDetectorRef and call this.cdr.detectChanges() after updating properties:

constructor(private cdr: ChangeDetectorRef) {}
// After setting data:
this.cdr.detectChanges();

2. SIDEBAR NAVIGATION

HTML Structure (app.component.html)

<nav class="sidebar">
  <div class="brand">
    <h1 class="t-heading">SAMURAI</h1>
    <span class="t-label">XWA - MODULE</span>
  </div>
  <ul class="nav-links">
    <li><a routerLink="/scanner" routerLinkActive="active" class="t-label">01 // SCANNER</a></li>
    <!-- ... -->
  </ul>
  <div class="sidebar-footer">
    <!-- Theme toggle, social links, version, dot-grid -->
  </div>
</nav>

SCSS Rules

  • Width: 250px fixed, border-right: 1px solid var(--border-visible)
  • Background: var(--surface)
  • Nav links: padding: var(--space-lg) var(--space-xl), border-bottom divider
  • Active link: background-color: var(--black), border-left: 2px solid var(--interactive)
  • Labels: Space Mono, ALL CAPS

Adding a New Nav Item

  1. Add <li> with <a routerLink="/newroute" routerLinkActive="active" class="t-label">05 // NEW</a> to app.component.html
  2. Add route to app.routes.ts with loadComponent
  3. Create feature folder under features/

3. SECTION HEADERS

Every feature page starts with a standard header:

<header class="section-header">
  <div>
    <h1 class="t-heading">PAGE_TITLE</h1>
    <span class="t-label">DESCRIPTIVE_SUBTITLE</span>
  </div>
</header>

SCSS:

.section-header {
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--space-md);
  margin-bottom: var(--space-xl);
}

4. BUTTONS

Export Action Buttons (Most Common Pattern)

<button class="btn-reset export-btn" type="button" (click)="action()" aria-label="Description">
  <svg class="export-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
    <!-- SVG path data -->
  </svg>
  <span>LABEL</span>
</button>

SCSS:

.export-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border-visible);
  background-color: transparent;
  color: var(--text-secondary);
  padding: var(--space-sm) var(--space-md);
  font-family: var(--font-data);
  font-size: var(--label);
  min-height: 40px;
  cursor: pointer;
  transition: all 0.2s ease;

  .export-icon {
    width: 16px;
    height: 16px;
    fill: currentColor;
    transition: color 0.2s ease;
    flex-shrink: 0;
  }

  &:hover {
    color: #FFD700;
    border-color: #FFD700;
  }

  &:disabled {
    opacity: 0.4;
    cursor: not-allowed;
  }
}

Destructive/Delete Buttons

<button type="button" class="btn-sm destructive" (click)="deleteAction()">DELETE</button>

SCSS:

.btn-sm {
  background-color: transparent;
  color: var(--text-primary);
  border: 1px solid var(--border-visible);
  font-family: var(--font-data);
  font-size: var(--body-sm);
  cursor: pointer;

  &.destructive:hover {
    background-color: var(--accent);
    color: var(--black);
    border-color: var(--accent);
  }
}

Theme Toggle Button

<button type="button" class="theme-toggle-btn" (click)="toggleTheme()">
  <svg><!-- moon/sun icon --></svg>
  <span>{{ label }}</span>
</button>

SCSS:

.theme-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  border: 1px solid var(--border-visible);
  background: var(--surface-raised);
  color: var(--text-primary);
  padding: 0.45rem 0.7rem;
  font-family: var(--font-data);
  font-size: var(--caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: all 0.2s ease;

  &:hover {
    opacity: 1;
    color: var(--success);
    border-color: var(--success);
  }
}

5. PANELS / CARDS

Standard container for feature content sections:

<section class="panel">
  <!-- content -->
</section>

SCSS:

.panel {
  padding: var(--space-lg);
  border: 1px solid var(--border-visible);
  background-color: var(--surface);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

6. INPUTS

Text Input (Underline Style)

<label class="t-label" for="input-id">LABEL</label>
<input type="text" id="input-id" [(ngModel)]="value" class="input-field">

SCSS:

.input-field {
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border-visible);
  color: var(--text-primary);
  font-family: var(--font-data);
  font-size: var(--body);
  padding: var(--space-sm) 0;

  &:focus {
    outline: none;
    border-bottom-color: var(--text-primary);
  }
}

Select/Dropdown

<select [(ngModel)]="selected" class="input-field">
  <option *ngFor="let opt of options" [value]="opt.value">{{opt.label}}</option>
</select>

7. STATUS INDICATORS

Status Text

<span class="t-label badge" [ngClass]="status === 'COMPLETED' ? 'text-success' : 'text-warning'">[STATUS]</span>

Severity Dots

.severity-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  &.high { background-color: var(--accent); }
  &.medium { background-color: var(--warning); }
  &.low { background-color: var(--success); }
  &.info { background-color: var(--interactive); }
  &.critical { background-color: var(--accent); }
}

8. LOADING STATE

Never use skeleton screens. Use text-based loading:

<div *ngIf="isLoading" class="t-label">Loading database...</div>
<div *ngIf="!isLoading && items.length === 0" class="empty-state t-label">
  [ NO DATA AVAILABLE ]
</div>

9. LAYOUT GRIDS

Dashboard Grid (2-column with sidebar)

.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xl);

  &.archive-grid {
    grid-template-columns: 350px 1fr;  // For list+detail layouts
  }
}

Export Actions Row

.export-actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
  flex-wrap: wrap;

  @media (max-width: 900px) {
    justify-content: center;
  }
}

10. ACCORDION (Details/Summary)

<details class="nothing-accordion">
  <summary>
    <span class="t-label">HEADER</span>
  </summary>
  <div class="accordion-content">
    <!-- Content -->
  </div>
</details>
.nothing-accordion {
  border: 1px solid var(--border-visible);
  background-color: var(--black);
  margin-bottom: var(--space-sm);

  summary {
    padding: var(--space-md);
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: var(--space-md);
    list-style: none;

    &:hover { background-color: var(--surface-raised); }
    &::-webkit-details-marker { display: none; }
  }

  .accordion-content {
    border-top: 1px solid var(--border-visible);
    padding: var(--space-md);
  }
}

11. API URL PATTERN

All HTTP requests use the current hostname with port 8000:

this.http.get<Type[]>(`http://${window.location.hostname}:8000/api/path`).subscribe({...});

WebSocket connections use the same pattern:

new WebSocket(`ws://${window.location.hostname}:8000/api/endpoint/live?param=value`);
database-schema

Samurai Design System -- Database Schema

This documents the PostgreSQL database schema used by Samurai.


1. ENTITY-RELATIONSHIP DIAGRAM

┌──────────┐          ┌──────────────────┐          ┌──────────┐
│  scans   │ 1───────*│ discovered_links  │ 1───────*│ findings │
│          │          │                  │          │          │
│ id (PK)  │          │ id (PK)          │          │ id (PK)  │
│ target   │          │ scan_id (FK)     │          │ scan_id  │
│ status   │          │ url              │          │ link_id  │
│ type     │          │ status_code      │          │ severity │
│ created  │          │ content_type     │          │ type     │
└──────────┘          └──────────────────┘          │ desc     │
      │                                              │ poc      │
      │ 1───────* (direct findings, link_id=NULL)   │ cvss     │
      └─────────────────────────────────────────────│          │
                                                     └──────────┘

A Scan can have:

  • Direct findings (where link_id is NULL) -- e.g., open ports, global recon results
  • discovered_links which each can have their own findings -- e.g., SQLi on a specific page

2. TABLE DEFINITIONS

2.1 scans

class Scan(Base):
    __tablename__ = "scans"
    id = Column(Integer, primary_key=True, index=True)
    domain_target = Column(String, index=True)
    status = Column(String, default="RUNNING")
    created_at = Column(DateTime, default=datetime.utcnow)
    scan_type = Column(String, default="port_scan")

| Column | Type | Constraints | Notes | |--------|------|------------|-------| | id | Integer | PK, auto-increment, indexed | Scan identifier | | domain_target | String(255) | Indexed | Target hostname/IP | | status | String | Default: RUNNING | One of: RUNNING, COMPLETED, ERROR, CANCELLED | | created_at | DateTime | Default: utcnow | Scan creation timestamp | | scan_type | String | Default: port_scan | Type identifier: port_scan:{profile}, crawler, web_recon |

2.2 discovered_links

class DiscoveredLink(Base):
    __tablename__ = "discovered_links"
    id = Column(Integer, primary_key=True, index=True)
    scan_id = Column(Integer, ForeignKey("scans.id", ondelete="CASCADE"))
    url = Column(String)
    status_code = Column(Integer, nullable=True)
    content_type = Column(String, nullable=True)

| Column | Type | Constraints | Notes | |--------|------|------------|-------| | id | Integer | PK, indexed | Link identifier | | scan_id | Integer | FK → scans.id, CASCADE | Parent scan | | url | String | Required | Full URL discovered | | status_code | Integer | Nullable | HTTP response code | | content_type | String | Nullable | HTTP Content-Type header |

2.3 findings

class Finding(Base):
    __tablename__ = "findings"
    id = Column(Integer, primary_key=True, index=True)
    scan_id = Column(Integer, ForeignKey("scans.id", ondelete="CASCADE"))
    link_id = Column(Integer, ForeignKey("discovered_links.id", ondelete="CASCADE"), nullable=True)
    severity = Column(String)
    finding_type = Column(String)
    description = Column(String)
    poc_payload = Column(String, nullable=True)  # Unlimited text (JSON/proof)
    cvss_score = Column(String, nullable=True)

| Column | Type | Constraints | Notes | |--------|------|------------|-------| | id | Integer | PK, indexed | Finding identifier | | scan_id | Integer | FK → scans.id, CASCADE | Parent scan | | link_id | Integer | FK → discovered_links.id, CASCADE, Nullable | NULL = global/direct finding | | severity | String | Required | info, low, medium, high, critical | | finding_type | String | Required | Type code: OPEN_PORT, SQL_INJECTION, REFLECTED_XSS, etc. | | description | String | Required | Human-readable description | | poc_payload | String | Nullable, unlimited length | Proof-of-concept or serialized JSON | | cvss_score | String | Nullable | CVSS score string |


3. RELATIONSHIPS & CASCADE RULES

# Scan has many Findings (direct)
findings = relationship("Finding", back_populates="scan",
    cascade="all, delete-orphan")

# Scan has many DiscoveredLinks
discovered_links = relationship("DiscoveredLink", back_populates="scan",
    cascade="all, delete-orphan")

# DiscoveredLink has many Findings
findings = relationship("Finding", back_populates="link",
    cascade="all, delete-orphan")

Cascade Behavior

  • Deleting a Scan → deletes all its discovered_links and findings
  • Deleting a DiscoveredLink → deletes all its child findings
  • These cascades are enforced at both ORM level (cascade="all, delete-orphan") and DB level (ondelete="CASCADE")

4. QUERY PATTERNS

List all scans (latest first):

scans = db.query(Scan).order_by(Scan.id.desc()).all()

Get scan with all relationships (eager loaded):

scan = db.query(Scan)\
    .options(
        joinedload(Scan.findings),
        joinedload(Scan.discovered_links)
            .joinedload(DiscoveredLink.findings)
    )\
    .filter(Scan.id == scan_id)\
    .first()

Export all data (full dump):

all_scans = db.query(Scan)\
    .options(
        joinedload(Scan.findings),
        joinedload(Scan.discovered_links)
            .joinedload(DiscoveredLink.findings)
    )\
    .order_by(Scan.id.desc())\
    .all()

Create new scan with findings:

scan = Scan(domain_target="example.com", status="RUNNING", scan_type="crawler")
db.add(scan)
db.commit()
db.refresh(scan)

finding = Finding(
    scan_id=scan.id,
    severity="high",
    finding_type="SQL_INJECTION",
    description="SQL injection in login form",
    cvss_score="8.5"
)
db.add(finding)
db.commit()

5. FINDING TYPES REFERENCE

Common finding_type values used across engines:

| finding_type | Engine | Description | |---|---|---| | OPEN_PORT | scanner | Nmap-discovered open port | | CONTACT_INFO_DISCLOSURE | scanner | Email/phone found on web surface | | UNSANITIZED_INPUT_CANDIDATE | scanner | Form input without sanitization | | REFLECTED_INPUT_ECHO | scanner | Probe value reflected in response | | web_recon_results | recon | Full recon results as JSON blob in poc_payload | | SQL_INJECTION | crawler | SQLMap or manual payload confirmation | | REFLECTED_XSS | crawler | XSS payload reflected in response | | MISSING_SECURITY_HEADER | crawler | Missing HSTS/CSP/etc. | | INSECURE_COOKIE | crawler | Missing Secure/HttpOnly flags | | EXPOSED_CONFIG | crawler | .env/.git/config accessible | | OPEN_CORS | crawler | Overly permissive CORS | | LFI_VULNERABILITY | crawler | Directory traversal | | INSECURE_LOGIN_FORM | crawler | Password over HTTP | | JS_SECRET_EXPOSURE | crawler | API keys/tokens in JS | | API_DISCOVERY | crawler | API docs/schema publicly exposed | | RISKY_HTTP_METHOD | crawler | PUT/DELETE allowed publicly | | SSL_TLS_ISSUE | crawler | SSL/TLS protocol vulnerabilities |

design-tokens

Samurai Design System -- Design Tokens

This documents the exact CSS custom properties used in /frontend/src/styles.scss.


1. TYPOGRAPHY

Font Stack

| Role | Font | Fallback | CSS | |------|------|----------|-----| | Display | "Doto" | "Space Mono", monospace | --font-display | | Body / UI | "Space Grotesk" | "DM Sans", system-ui, sans-serif | --font-body | | Data / Labels | "Space Mono" | "JetBrains Mono", "SF Mono", monospace | --font-data |

Type Scale

| Token | Size | Line Height | Letter Spacing | Use | |-------|------|-------------|----------------|-----| | --display-xl | 72px | 1.0 | -0.03em | Hero numbers | | --display-lg | 48px | 1.05 | -0.02em | Section heroes | | --display-md | 36px | 1.1 | -0.02em | Page titles | | --heading | 24px | 1.2 | -0.01em | Section headings | | --subheading | 18px | 1.3 | 0 | Subsections | | --body | 16px | 1.5 | 0 | Body text | | --body-sm | 14px | 1.5 | 0.01em | Secondary body | | --caption | 12px | 1.4 | 0.04em | Timestamps, footnotes | | --label | 11px | 1.2 | 0.08em | ALL CAPS monospace labels |

Typography Utility Classes

| Class | Font | Size | Weight | |-------|------|------|--------| | .t-display-xl | Doto | 72px | Variable, tight tracking | | .t-display-lg | Doto | 48px | Variable | | .t-display-md | Doto | 36px | Variable | | .t-heading | Space Grotesk | 24px | Regular | | .t-label | Space Mono | 11px | ALL CAPS | | .t-data | Space Mono | inherit | Regular |

Typographic Rules (Hard Constraints)

  • Doto: 36px+ only, tight tracking, never for body text
  • Labels: Always Space Mono, ALL CAPS, 0.08em spacing, 11px
  • Data/Numbers: Always Space Mono
  • Hierarchy: display (Doto) > heading (Space Grotesk) > label (Space Mono caps) > body (Space Grotesk)

2. COLOR SYSTEM

Primary Palette (Dark Mode -- Default)

| Token | Hex | Role | |-------|-----|------| | --black | #000000 | Primary background (OLED) | | --surface | #111111 | Elevated surfaces, cards | | --surface-raised | #1A1A1A | Secondary elevation | | --border | #222222 | Subtle dividers (decorative only) | | --border-visible | #333333 | Intentional borders, wireframe lines | | --text-disabled | #666666 | Disabled text | | --text-secondary | #999999 | Labels, captions, metadata | | --text-primary | #E8E8E8 | Body text | | --text-display | #FFFFFF | Headlines, hero numbers |

Light Mode Overrides

| Token | Dark | Light | |-------|------|-------| | --black | #000000 | #F5F5F5 | | --surface | #111111 | #FFFFFF | | --surface-raised | #1A1A1A | #F0F0F0 | | --border | #222222 | #E8E8E8 | | --border-visible | #333333 | #CCCCCC | | --text-disabled | #666666 | #999999 | | --text-secondary | #999999 | #666666 | | --text-primary | #E8E8E8 | #1A1A1A | | --text-display | #FFFFFF | #000000 | | --interactive | #5B9BF6 | #007AFF |

Light mode is activated by adding class .theme-light to <body>.

Accent & Status Colors (Identical Across Modes)

| Token | Hex | Usage | |-------|-----|-------| | --accent | #D71921 | Destructive, urgent, active states | | --accent-subtle | rgba(215,25,33,0.15) | Accent tint backgrounds | | --success | #4A9E5C | Completed, connected | | --warning | #D4A843 | Caution, pending | | --error | #D71921 | Shares accent red | | --interactive | #5B9BF6 (dark) / #007AFF (light) | Links, picker values |

Utility Classes for Colors

  • .text-accentcolor: var(--accent)
  • .text-successcolor: var(--success)
  • .text-warningcolor: var(--warning)
  • .bg-surfacebackground-color: var(--surface)
  • .border-dividerborder: 1px solid var(--border)
  • .border-visibleborder: 1px solid var(--border-visible)

3. SPACING

Spacing Scale (8px base)

| Token | Value | Use | |-------|-------|-----| | --space-2xs | 2px | Optical adjustments only | | --space-xs | 4px | Icon-to-label gaps, tight padding | | --space-sm | 8px | Component internal spacing | | --space-md | 16px | Standard padding, element gaps | | --space-lg | 24px | Group separation | | --space-xl | 32px | Section margins | | --space-2xl | 48px | Major section breaks | | --space-3xl | 64px | Page-level vertical rhythm | | --space-4xl | 96px | Hero breathing room |

Spacing as Meaning

Tight (4-8px)   = "These belong together"
Medium (16px)   = "Same group, different items"
Wide (32-48px)  = "New group starts here"
Vast (64-96px)  = "This is a new context"

4. MOTION & INTERACTION

  • Duration: 150-250ms micro, 300-400ms transitions
  • Easing: cubic-bezier(0.25, 0.1, 0.25, 1) -- subtle ease-out. No spring/bounce.
  • Hover: border/text brightens. No scale, no shadows.
  • Theme transition: cubic-bezier(0.16, 1, 0.3, 1) over 260ms on color properties
  • Route animation: fadeInSlideUp 400ms, opacity + translateY(10px → 0)

5. DOT-MATRIX MOTIF

Two utility classes for dot-grid backgrounds:

.dot-grid {
  background-image: radial-gradient(circle, var(--border-visible) 1px, transparent 1px);
  background-size: 16px 16px;
}
.dot-grid-subtle {
  background-image: radial-gradient(circle, var(--border) 0.5px, transparent 0.5px);
  background-size: 12px 12px;
}
  • dot-grid-subtle is used on the main content area background
  • dot-grid is used for decorative surface treatments
  • Never use dot-grid as container border or button style

6. EXPORT BUTTON GOLD

Export buttons use a specific hover color across the entire application:

  • Hover text/border: #FFD700 (gold)
  • This is a project-level convention for export/download actions

7. RESPONSIVE BREAKPOINTS

| Breakpoint | Target | |-----------|--------| | 980px | Sidebar collapses to top bar, nav becomes 2-column grid | | 900px | Export actions center-align | | 640px | Nav becomes single column, main padding reduces |

export-patterns

Samurai Design System -- Export Patterns

This documents the export system architecture, both existing client-side exports and the new database export feature.


1. CLIENT-SIDE EXPORT (Existing)

Architecture

All existing exports are client-side only -- no backend involved. The browser generates the file and triggers a download via Blob + URL.createObjectURL.

Export Component Pattern

Each feature has its own export-actions component:

features/scanner/components/export-actions/     → scanner-export-actions.component.*
features/recon/components/export-actions/        → recon-export-actions.component.*
features/vulnerabilities/.../findings-export-actions/ → findings-export-actions.component.*

Each follows the same template:

  • 4 buttons: CSV, JSON, PDF, BIN
  • Component receives conditions via @Input() (e.g., hasExports, findingsCount)
  • Emits events via @Output() (e.g., exportCsv, exportJson, exportPdf, exportBinary)
  • Parent feature component handles the actual export logic

Button HTML

<button class="btn-reset export-btn" type="button" (click)="exportJson.emit()" aria-label="Export as JSON">
  <svg class="export-icon" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
    <!-- JSON icon path -->
  </svg>
  <span>EXPORT JSON</span>
</button>

Note: Each format has a distinct SVG icon (different paths).

Button SCSS (Identical across all export-actions)

.export-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border-visible);
  background-color: transparent;
  color: var(--text-secondary);
  padding: var(--space-sm) var(--space-md);
  font-family: var(--font-data);
  font-size: var(--label);
  min-height: 40px;
  cursor: pointer;
  transition: all 0.2s ease;

  .export-icon {
    width: 16px;
    height: 16px;
    fill: currentColor;
    flex-shrink: 0;
  }

  &:hover { color: #FFD700; border-color: #FFD700; }
  &:disabled { opacity: 0.4; cursor: not-allowed; }
}

Parent Feature Export Implementation

JSON Export (simplest):

exportAsJson(payload: any, filename: string): void {
  const json = JSON.stringify(payload, null, 2);
  const blob = new Blob([json], { type: 'application/json' });
  this.downloadBlob(blob, `${filename}.json`);
}

CSV Export:

exportAsCsv(rows: any[], headers: string[], filename: string): void {
  const escape = (v: string) => `"${String(v).replace(/"/g, '""')}"`;
  const csv = [headers.join(','), ...rows.map(r => headers.map(h => escape(r[h])).join(','))].join('\n');
  const blob = new Blob([csv], { type: 'text/csv' });
  this.downloadBlob(blob, `${filename}.csv`);
}

PDF Export (uses jsPDF + jspdf-autotable):

import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';

exportAsPdf(rows: any[], headers: string[], title: string, filename: string): void {
  const doc = new jsPDF('landscape', 'mm', 'a4');
  doc.setFillColor(0, 0, 0);
  doc.rect(0, 0, 297, 210, 'F');
  doc.setTextColor(255, 255, 255);
  // Title, metadata, table...
  doc.save(`${filename}.pdf`);
}

Binary Export (uses pako for gzip):

import * as pako from 'pako';

exportAsBinary(payload: any, filename: string): void {
  const json = JSON.stringify(payload, null, 2);
  const compressed = pako.gzip(json);
  const blob = new Blob([compressed], { type: 'application/octet-stream' });
  this.downloadBlob(blob, `${filename}.bin`);
}

Universal Download Helper:

private downloadBlob(blob: Blob, filename: string): void {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(url);
}

2. DATABASE EXPORT (New Feature)

Backend Endpoints

GET /api/database/export/raw -- Returns full database dump as downloadable JSON:

{
  "export_metadata": {
    "exported_at": "2026-05-30T12:00:00Z",
    "samurai_version": "2.5.0",
    "scan_count": 15,
    "finding_count": 142,
    "link_count": 89
  },
  "scans": [
    {
      "id": 1,
      "domain_target": "example.com",
      "status": "COMPLETED",
      "scan_type": "crawler",
      "created_at": "2026-05-29T10:00:00Z",
      "findings": [...],
      "discovered_links": [...]
    }
  ]
}

POST /api/database/export/encrypted -- Returns AES-encrypted JSON:

  • Request body: { "password": "user-entered-password" }
  • Response: Binary file with AES-encrypted content
  • Encryption: AES-256-GCM via PBKDF2 key derivation

Frontend Feature

New page at /export accessible from sidebar 05 // EXPORT DB.

Two modes:

  1. Direct export: Single click downloads samurai-database-export-{timestamp}.json
  2. Encrypted export: Enter password → submit → downloads samurai-database-export-{timestamp}.bin.enc

UI Layout:

  • Left panel: Export mode selection + description
  • Right panel: Action area (download button or password form)
  • Export buttons follow the gold hover pattern

3. FILE NAMING CONVENTION

| Export Type | Filename Pattern | |---|---| | Scanner JSON | samurai-scanner-{scanId}.json | | Recon JSON | samurai-recon-{target}.json | | Findings JSON | samurai-findings-scan-{scanId}.json | | DB Raw Export | samurai-database-export-{YYYY-MM-DD}.json | | DB Encrypted | samurai-database-export-{YYYY-MM-DD}.bin.enc |