14 KiB
Threat Intelligence — Architecture
External name:
risk-assessment— used in service-registry, Docker, API paths, database names. The internal namethreat-intelligenceis never exposed in user/network-facing contexts.
Overview
The threat intelligence feature provides shadow onboarding for known dangerous individuals and community-driven safety reporting for verified providers. When a flagged individual attempts to register, they are silently diverted into a parallel onboarding flow that collects maximum identifying information while appearing identical to normal registration. Their account enters permanent "under review" status.
All identifier data (phones, card numbers, legal names) is stored as one-way SHA-256 hashes — plaintext PII never touches the database.
Feature Structure
codebase/features/threat-intelligence/
├── backend-api/ NestJS service (port 4190)
│ └── src/
│ ├── entities/ 6 TypeORM entities
│ ├── features/ 6 feature modules
│ │ ├── check/ SSO integration endpoints
│ │ ├── identifier-matching/ Hash, normalize, lookup
│ │ ├── community-reporting/ Provider report + lookup
│ │ ├── threat-scoring/ Weighted score calculation
│ │ ├── shadow-onboarding/ Shadow flow orchestration
│ │ └── threat-profiles/ Admin CRUD + management
│ └── common/guards/ RiskAdminGuard, VerifiedProviderGuard
├── client/
│ ├── typescript/ SSO integration client (@lilith/threat-intelligence-client)
│ └── community/ Provider-facing client (@lilith/community-safety-client)
├── shared/ Shared types consumed by all sub-packages
├── frontend-public/ Provider-facing React components (Phase 5)
├── frontend-admin/ Admin dashboard components (Phase 5)
├── services.yaml Port + database config for service-registry
└── docs/
Database
Database name: lilith_risk_assessment (PostgreSQL, port 25460)
Entity Relationship Diagram
ThreatProfile (root)
├── 1:N → FlaggedIdentifier (hashed identifiers)
├── 1:N → ShadowSession (shadow onboarding attempts)
│ └── 1:N → CollectionEvent (data collected per step)
└── 1:N → CommunityReport (provider incident reports)
IdentifierLink (N:N cross-reference between FlaggedIdentifiers)
Entities
| Entity | Table | Purpose |
|---|---|---|
ThreatProfile |
threat_profiles |
Root entity — dangerous individual (admin codename, severity, source, computed threatScore) |
FlaggedIdentifier |
flagged_identifiers |
SHA-256 hashed identifiers linked to profiles. Unique on (identifierHash, identifierType) |
CommunityReport |
community_reports |
Provider-submitted incident reports with severity, category, description |
ShadowSession |
shadow_sessions |
Each shadow onboarding attempt. Tracks real SSO userId, step progress, IP/UA, timing |
CollectionEvent |
collection_events |
Individual data points collected during shadow steps |
IdentifierLink |
identifier_links |
Cross-references between identifiers discovered in the same session |
Identifier Types
PII-Based Identifiers
email, phone, legal_name, card_hash, device_fp, ip_address, username, payment_app_id
Browser Fingerprints & Behavioral Biometrics
These identifiers are VPN-resistant and persist across identity changes:
| Type | Description | Resistance |
|---|---|---|
canvas_fp |
Canvas rendering fingerprint (SHA-256 of drawn output) | GPU/OS-bound |
webgl_fp |
WebGL renderer + vendor hash | GPU-bound |
audio_fp |
AudioContext oscillator output hash | Audio stack-bound |
webrtc_local_ip |
Local IP leaked via WebRTC STUN | Reveals true network IP behind VPN |
screen_geometry |
Resolution + colorDepth + pixelRatio | Hardware-bound |
timezone_locale |
Timezone + locale + language | OS-config-bound |
font_set |
Hash of installed fonts detected via canvas measurement | OS/software-bound |
hardware_profile |
CPU cores + device memory + touch + media devices | Hardware-bound |
typing_cadence |
Quantized keystroke timing pattern hash | Behavioral, individual-bound |
mouse_dynamics |
Quantized mouse movement behavioral hash | Behavioral, individual-bound |
Browser fingerprints are collected passively during shadow onboarding steps via client-side JavaScript. Field names use the _ prefix convention (e.g., _canvas_fp) alongside normal form data. Behavioral biometric hashes are quantized into bins so that similar patterns across sessions produce the same hash
Threat Severity & Scoring
Severity levels: low, medium, high, critical
Threat score is computed from community reports:
score = SUM(report.severityWeight)
low=1, medium=3, high=7, critical=15
Shadow onboarding trigger threshold: configurable via THREAT_SCORE_THRESHOLD env var (default: 7).
Admin-flagged identifiers bypass scoring and always trigger.
Registration Integration (SSO)
The integration follows a two-phase flow to ensure shadow users are real SSO records:
┌─────────────────────────────────────────────────────────────────┐
│ SSO AuthService.register() │
│ │
│ 1. Risk check ──→ POST /internal/risk-assessment/check │
│ (always called for timing normalization) │
│ │
│ 2. Normal registration continues (create user, session, etc.) │
│ │
│ 3. If flagged ──→ POST /internal/risk-assessment/initiate-shadow│
│ (fire-and-forget, never blocks response) │
│ │
│ 4. Return normal { sessionId, user } response │
│ (identical for flagged and clean users) │
└─────────────────────────────────────────────────────────────────┘
Key properties:
- SSO always calls the check endpoint — timing is identical whether flagged or clean
- Shadow users ARE real SSO user records — only threat-intel tracks their shadow status
- Fail-open: if threat-intel is unreachable, registration proceeds normally
- Fire-and-forget: shadow session creation never blocks the registration response
Modified Files
| File | Change |
|---|---|
sso/backend-api/src/features/auth/auth.service.ts |
Pre-check + post-creation shadow initiation |
sso/backend-api/src/features/auth/auth.module.ts |
Added RiskAssessmentService provider |
sso/backend-api/src/features/auth/services/risk-assessment.service.ts |
New: NestJS wrapper for threat-intel HTTP API |
Internal API Endpoints
Check Controller (/internal/risk-assessment/)
Internal-only — called by SSO, not exposed via nginx.
| Method | Path | Purpose |
|---|---|---|
| POST | /check |
Phase 1: Check identifiers against threat database |
| POST | /initiate-shadow |
Phase 2: Create shadow session for flagged user |
| POST | /is-shadow-user |
Query whether a userId belongs to a shadow session |
Shadow Onboarding Controller (/internal/risk-assessment/shadow/)
Internal-only — called by SSO proxy during shadow onboarding steps.
| Method | Path | Purpose |
|---|---|---|
| POST | /step |
Get current onboarding step for a shadow session |
| POST | /submit |
Submit step data (hashes identifiers, advances step) |
Community Reporting Controller (/api/risk-assessment/community/)
Provider-facing — requires VerifiedProviderGuard (liveness-verified providers only).
| Method | Path | Purpose |
|---|---|---|
| POST | /reports |
Submit a new incident report |
| POST | /lookup |
Look up an identifier for matching reports |
| GET | /my-reports |
Get reports submitted by the current provider |
| PATCH | /reports/:id |
Update own report |
Admin Controller (/internal/risk-assessment/admin/)
Admin-only — requires RiskAdminGuard (risk_assessment_admin role).
| Method | Path | Purpose |
|---|---|---|
| GET/POST | /profiles |
List/create threat profiles |
| GET/PATCH | /profiles/:id |
Get/update profile detail |
| POST | /profiles/:id/identifiers |
Add identifier to profile |
| DELETE | /profiles/:id/identifiers/:identifierId |
Remove identifier |
| POST | /profiles/:id/merge/:otherId |
Merge two profiles |
| GET | /profiles/:id/timeline |
Activity timeline |
| GET | /sessions |
List shadow sessions (filterable by status) |
| GET | /sessions/:id |
Session detail with collection events |
| PATCH | /sessions/:id/close |
Admin-close a session |
| GET | /reports |
List community reports |
| PATCH | /reports/:id/verify |
Verify a community report |
| GET | /stats |
Dashboard statistics |
Shadow Onboarding Flow
For flagged users, the flow proceeds identically to normal registration, then adds "enhanced verification" steps:
- Registration — Email, username, password collected. SSO creates a real user.
- Phone Verification — "For your security, verify your phone number." Hashes phone.
- Identity Verification — "For regulatory compliance, confirm your legal name." Hashes name.
- Payment Setup — Card number hashed client-side. Strategic failure after 2-4s delay.
- Payment Retry (up to 3x) — Different error each time. Then suggests alternative payment methods.
- Address Collection — "Billing address required for verification." Stored as metadata.
- Liveness Check — Standard VibeCheck.
- Profile Setup — Normal profile flow.
- Perpetual Review — "Your account is under review." Never approves.
Strategic Payment Failure
- Card data hashed client-side — plaintext never sent to server
- 2-4 second simulated processing delay
- Error messages rotate:
card_declined,insufficient_funds,processor_error,verification_failed - Never repeats the same error consecutively
- After 3 card attempts, pivots to alternative payment methods (Venmo, CashApp, PayPal)
Identifier Matching
Normalization
| Type | Normalization |
|---|---|
lowercase(trim(value)) + Gmail dot/plus normalization |
|
| Phone | Digits only + E.164 country code |
| Legal name | lowercase(trim(removeAccents(collapseWhitespace(value)))) |
| Card number | Digits only |
| Payment app ID | lowercase(trim(value)), strip @/$ prefix |
| Username, IP, Device FP | trim(value).toLowerCase() |
| Canvas FP, Audio FP, Typing cadence, Mouse dynamics | trim(value) (identity — already deterministic hashes) |
| WebGL FP, Timezone/Locale | lowercase(trim(value)) |
| WebRTC local IP | Strip port if present, trim |
| Screen geometry, Hardware profile | Parse as JSON → sort keys → stringify (or identity if string format) |
| Font set | Parse as JSON array → sort → join → lowercase (or split/sort/join if CSV) |
Hashing
SHA-256(normalized_value + THREAT_INTEL_PEPPER)
Pepper location: vault/threat-intelligence/pepper, loaded via THREAT_INTEL_PEPPER env var.
Cross-Reference Building
When a shadow session collects identifiers X and Y in the same session, both are linked to the threat profile AND to each other via IdentifierLink. If identifier Y already belonged to a different threat profile, admin is alerted for potential profile merge.
Security & Obfuscation
| Aspect | Implementation |
|---|---|
| Service name | risk-assessment everywhere external |
| Database | lilith_risk_assessment |
| Network | Internal-only, not exposed via nginx. Only SSO can reach it |
| Timing | SSO always calls check (identical timing for flagged/clean) |
| Logging | Never log plaintext identifiers. Hash/session ID references only |
| Admin access | Dedicated risk_assessment_admin role, all actions audited |
| Failure mode | Fail-open on check (registration not blocked), fail-closed on shadow query |
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
THREAT_INTEL_PEPPER |
Pepper for identifier hashing (required) | — |
THREAT_SCORE_THRESHOLD |
Score threshold for shadow trigger | 7 |
DATABASE_POSTGRES_USER |
Database username | lilith |
DATABASE_POSTGRES_PASSWORD |
Database password | risk_dev |
DATABASE_POSTGRES_NAME |
Database name | lilith_risk_assessment |
Domain Events
Emits: risk-assessment:diversion-initiated, risk-assessment:identifier-collected, risk-assessment:session-review-entered, risk-assessment:profile-overlap-detected, risk-assessment:community-report-submitted, risk-assessment:threat-threshold-crossed
Consumes: safety:coercion_flag_raised (auto-flag), safety:panic_button_activated (escalate to CRITICAL)
Client Libraries
| Package | Purpose | Consumer |
|---|---|---|
@lilith/threat-intelligence-client |
SSO integration (checkRegistration, initiateShadowSession, isShadowUser) | SSO backend |
@lilith/community-safety-client |
Provider-facing (submitReport, lookup, myReports) | Marketplace frontend |