Sec Ops Studio, an all in one SOC toolkit.
Purple Sentinel Studio is a fullstack analyst workspace for:
- IOC intake and tracking
- OSINT-style enrichment with attribution
- Quick utilities (IP range, Geo-IP, hashing, JSON prettifier, timeline builder)
- React + Vite (TypeScript)
- Node + Express (TypeScript)
- PostgreSQL
/loginSecure authentication entrypoint/Overview dashboard/iocsIOC Vault/toolsQuick Tools workspace (selector-based)/feedsThreat Feeds workspace/adminAdministration workspace (user accounts + audit trail, administrator role)/incidentsIncident Manager (declare incident IDs and metadata)/timeline-builderDedicated Timeline Builder workspace/incident-reportsIncident report generation workspace/incident-graphDedicated graph view (opened in a new tab from Incident Reports)
Purple Sentinel SOC Studio is designed to run only via containers (nginx + backend services + Postgres).
cd /PurpleSentinelStudio
cp .env.example .env
# Recommended: set strong secrets (examples)
echo "JWT_SECRET=\"$(openssl rand -hex 64)\"" >> .env
echo "PSS_MASTER_KEY=\"$(openssl rand -base64 32)\"" >> .env
echo "DEFAULT_ADMIN_PASSWORD=\"change-this-to-a-strong-password\"" >> .env
docker compose up -d --build --remove-orphansOpen the UI:
- https://localhost:8443 (self-signed)
HTTP (redirects to HTTPS):
Health check:
curl -sk https://localhost:8443/api/healthDocs:
containerisation.md(deploy/run guide)CONTAINER_INFRA.md(architecture + routing)
In Docker Compose, nginx routes API traffic by path:
/api/auth/*,/api/profile/*,/api/audit/*,/api/settings/*→auth-service(:4001)/api/iocs/*,/api/ioc-enrichment/*→ioc(:4005)/api/tools/*→tools(:4010)- everything else under
/api/*→core-api(:4000)
To prevent drift across backend services, common Express setup now lives in:
server/src/utils/expressBootstrap.ts
This module centralizes:
- standard middleware bootstrap (
helmet,cors, JSON body limit,morgan, trust proxy handling) - shared authenticated mutating-request audit middleware
- shared 404 and error-response handlers
- consistent
SIGTERM/SIGINTgraceful shutdown registration so each service drains HTTP traffic and closes PostgreSQL cleanly
All Node services (core-api, auth-service, ioc, tools) import this module so middleware behavior and error handling remain consistent.
When running via Docker Compose, all API calls go through nginx at:
-
https://localhost:8443/api/...(self-signed) -
GET /api/healthcore API health check -
GET /api/health/intelintel provider reachability probe -
GET /api/health/componentsaggregated component availability (administrator) -
POST /api/auth/loginsign in and receive access token (token_typeisDPoPwhen adpop_jwkis supplied; otherwiseBearer) -
POST /api/auth/mfa/completecomplete MFA login challenge and receive access token -
POST /api/auth/logoutsign out (audited) -
GET /api/auth/meget current authenticated user profile + permissions -
GET /api/auth/mfa/statusget current account MFA eligibility/status -
POST /api/auth/mfa/enroll/startbegin TOTP enrollment and return QR payload -
POST /api/auth/mfa/enroll/verifyverify TOTP enrollment code and enable MFA -
POST /api/auth/mfa/disabledisable MFA for current account (requires password + TOTP) -
GET /api/profile/meget the current user's directory profile (full name, email, avatar) -
PUT /api/profile/meupdate the current user's profile avatar (avatar_data_url); full name/email are administrator-managed -
GET /api/profile/userslist active user directory profiles (authenticated users) -
GET /api/profile/preferencesget per-user UI preferences (sound + accent theme) -
PUT /api/profile/preferencesupdate per-user UI preferences -
GET /api/auth/userslist user accounts (administrator, paginated)- Query params:
search,limit(default5),offset - Response:
{ users, total, limit, offset }
- Query params:
-
POST /api/auth/userscreate user account (administrator):username,display_name,role,user_type(application_userorapi_bot),password -
PATCH /api/auth/users/:idupdate user account + directory profile (administrator):username,display_name(full name),role,is_active,password,email,avatar_data_url(note:user_typeis configured at creation) -
DELETE /api/auth/users/:iddelete user account (administrator) -
GET /api/auth/roleslist roles + permissions (administrator) -
POST /api/auth/rolescreate role (administrator) -
PATCH /api/auth/roles/:nameupdate role permissions/metadata (administrator) -
DELETE /api/auth/roles/:namedelete role (administrator; non-system roles only) -
GET /api/iocslist IOCs (supportsq,type,status,severity,enrichment_status; includelimit+offsetfor pagination, returning{ iocs, total, limit, offset }) -
POST /api/iocscreate IOC -
GET /api/iocs/:idIOC detail + enrichment -
PATCH /api/iocs/:idupdate IOC -
DELETE /api/iocs/:idremove IOC -
POST /api/iocs/:id/enrichrun enrichment and attribution (legacy alias) -
GET /api/ioc-enrichment/:iocIdget enrichment snapshot + attribution for one IOC -
GET /api/ioc-enrichment/:iocId/attributionget attribution-only payload -
POST /api/ioc-enrichment/:iocId/runrun enrichment for one IOC and persist results -
GET /api/ioc-enrichment/pending/statusget pending IOC enrichment count -
POST /api/ioc-enrichment/pending/runprocess pending IOC enrichment in batch (limit1-200) -
GET /api/settings/integrationslist enrichment integration settings and key status -
GET /api/settings/integrations/:providerget one integration setting -
PUT /api/settings/integrations/:providerset integration enabled state and/or API key -
DELETE /api/settings/integrations/:provider/keyremove a stored API key and disable provider -
GET /api/audit/eventsretrieve audit trail events (administrator, paginated)- Query params:
limit(default200, max500),offset - Response:
{ events, total, limit, offset }
- Query params:
-
GET /api/incidentslist incidents -
GET /api/incidents/:incidentIdincident detail -
POST /api/incidentsdeclare incident -
PATCH /api/incidents/:incidentIdupdate incident metadata/status -
DELETE /api/incidents/:incidentIddelete incident (cascades timeline entries) -
GET /api/tools/ip-range?cidr=CIDR calculator -
GET /api/tools/geoip?ip=Geo-IP lookup -
GET /api/tools/threat-feedscurated open-source threat feed catalog -
POST /api/tools/hashhashing utilities -
POST /api/tools/url-parseURL/domain parser -
GET /api/tools/dns?name=&type=DNS lookup -
POST /api/tools/encodebase64/url encode/decode -
POST /api/tools/json-prettifyJSON pretty/minified formatter + flattened key paths -
POST /api/tools/timestamptimestamp converter -
POST /api/tools/extract-iocsregex IOC extraction -
GET /api/tools/http-requestslist stored outbound HTTP request history (per user) -
GET /api/tools/http-requests/:idretrieve one stored request + response (per user) -
POST /api/tools/http-requestsexecute + store an outbound HTTP request (audited; sensitive headers are redacted at rest) -
POST /api/tools/entropypassword entropy estimate -
POST /api/tools/magicfile signature detection -
POST /api/tools/jwt-decodedecode JWT header/payload -
POST /api/tools/whoisRDAP/ASN lookup -
POST /api/tools/subnet-overlapCIDR overlap checker -
POST /api/tools/cidr-summarizeCIDR/IP summarizer -
POST /api/tools/defangIOC defang/refang -
POST /api/tools/stix-patternSTIX pattern generator -
POST /api/tools/sigma-ruleSigma YAML generator -
POST /api/tools/pcap-filterWireshark/tcpdump filter snippets -
POST /api/tools/mitre-maptag-to-ATT&CK mapper -
POST /api/tools/risk-scoreweighted risk calculator -
GET /api/tools/timeline-entries?incident_id=&limit=incident timeline entries -
POST /api/tools/timeline-entriescreate a structured timeline entry -
DELETE /api/tools/timeline-entries/:iddelete a timeline entry -
POST /api/tools/timeline-normalizeevent timeline normalizer -
POST /api/tools/http-headers-analyzeHTTP header analyzer -
POST /api/tools/tls-inspectTLS certificate inspector -
POST /api/tools/user-agent-parseuser-agent parser -
POST /api/tools/email-headers-analyzeemail header analyzer -
POST /api/tools/file-anomalyfilename anomaly detection -
POST /api/tools/regex-testregex tester with presets
- Endpoint:
GET /api/tools/threat-feeds - Query params (optional):
category,format,access,search - Returns:
feedslist of curated open-source feeds (public and registration-based)categories,formats,access_levelsfor filter controls
- Base route:
/api/ioc-enrichment - Purpose: Dedicated service for enrichment and attribution of IOCs stored in PostgreSQL.
- Storage behavior:
- writes provider outputs into
enrichments - updates IOC fields in
iocs(enrichment_status,attribution,last_enriched_at,updated_at) - mode behavior:
- uses live provider APIs when enabled keys are configured
- gracefully falls back to deterministic mock enrichment if no live provider succeeds
- Example batch request:
{
"limit": 50
}- Base route:
/api/settings/integrations - Supported providers:
virustotalabuseipdbotx(AlienVault OTX)shodan- API keys are encrypted before storage in PostgreSQL and only masked hints are returned to the frontend.
- Optional env vars:
PSS_MASTER_KEY(preferred): 32-byte secret (hex or base64) for API-key encryptionSETTINGS_KEY_PATH: override path for generated local encryption key file
- Authentication model: JWT access token
- Optional MFA: TOTP (Google Authenticator-compatible) for
application_useraccounts. Enrollment returns anotpauth://URI and QR code image; MFA is completed with a 6-digit code before token issuance. - Default browser flow uses a DPoP-style proof-of-possession handshake (nonce + per-request signature) to reduce bearer token replay if a JWT is stolen.
- Login accepts optional
dpop_jwk(public P-256 JWK). When supplied, the server binds the JWT to that key viacnf.jktand returnstoken_type: "DPoP"plus an initial nonce (dpop_nonce, also mirrored in theDPoP-Nonceresponse header). - If MFA is enabled for the account,
POST /api/auth/loginreturns{ mfa_required, challenge_id, challenge_expires_at }; the client then submitsPOST /api/auth/mfa/completewith the challenge + TOTP code to obtain the JWT. - DPoP requests use
Authorization: DPoP <token>plus aDPoP: <proofJWT>header containing a signed proof withhtm,htu,iat,jti, andnonce. - The server rotates nonces via
DPoP-Nonceon successful requests and the client automatically updates and retries once on401when it receives a fresh nonce. - Bearer tokens remain supported for non-browser clients: omit
dpop_jwkand useAuthorization: Bearer <token>. - For a detailed breakdown of JWT behavior and where it is enforced, see
JWT.md. - Role-based access control enforced in backend and frontend route guards.
- Role-based access control is enforced by the backend on every request using the role-permission mapping stored in PostgreSQL (
rolestable). - Access token lifetime is short by default (15 minutes) and is refreshed automatically during active use via
X-PS-Auth-Tokenresponse headers. Configure viaJWT_TTL_SECONDSandJWT_REFRESH_WINDOW_SECONDS. - The browser client enforces an inactivity timeout (default 15 minutes) and clears local auth state. Configure via
VITE_IDLE_TIMEOUT_MSorVITE_IDLE_TIMEOUT_MINUTES. - Default roles are seeded on startup (administrator, incident_commander, soc_analyst, threat_intel, forensics, responder, viewer) and can be extended/modified via
GET/POST/PATCH /api/auth/roles. - On first start, a default administrator is auto-created if no admin exists:
- username from
DEFAULT_ADMIN_USERNAME(defaultadmin) - password from
DEFAULT_ADMIN_PASSWORDif provided (minimum 12 chars), otherwise generated at startup and printed to server logs - Optional token signing config:
JWT_SECRET(recommended for stable JWT signing secret)JWT_SECRET_PATH(path for generated signing secret whenJWT_SECRETis not set)
- Every authenticated mutating API call (
POST,PUT,PATCH,DELETE) is recorded inaudit_events. - Auth login success/failure and logout are also audited.
- Audit logs are viewable via
/api/audit/eventsand the Administration UI for administrator users only.
-
Endpoints:
-
GET /api/tools/timeline-entries?incident_id=IR-2026-0001&limit=250 -
POST /api/tools/timeline-entries -
DELETE /api/tools/timeline-entries/:id -
Purpose: Store incident timeline events as structured JSON in PostgreSQL (
timeline_entriestable). -
Requirement: Incident must exist in Incident Manager (
incidentstable) before timeline entries can be created or loaded. -
Entry model supports:
-
entry_typefor system events, analyst actions, communications, and stage transitions -
irp_stageand optionaltransition_to_stagefor IRP phase changes -
operational metadata: source system, actor, role, channel, audience, case reference, tags, evidence refs
-
metadataobject for custom structured fields -
Example create request:
{
"incident_id": "IR-2026-0001",
"occurred_at": "2026-02-11T14:12:00Z",
"entry_type": "communication",
"irp_stage": "containment",
"transition_to_stage": "eradication",
"title": "IR lead approved transition to eradication",
"summary": "Containment validation completed; eradication starts immediately.",
"source_system": "IR coordination",
"actor_name": "Jane Analyst",
"actor_role": "IR Lead",
"communication_channel": "War room bridge",
"audience": "SOC lead, IT ops lead, legal",
"ticket_ref": "INC-2026-0198",
"tags": ["irp", "stage-transition", "communications"],
"evidence_refs": ["ticket://INC-2026-0198", "notes://warroom/2026-02-11"],
"metadata": {
"approval": "granted",
"change_window": "immediate"
}
}- Endpoint:
POST /api/tools/json-prettify - Request body:
{
"value": "{\"ioc\":\"8.8.8.8\",\"tags\":[\"dns\",\"beacon\"],\"meta\":{\"source\":\"hunt\"}}",
"indent": 2
}- Response includes:
formatted(pretty JSON)minified(single-line JSON)keys(flattened key paths in dot notation, including arrays with[], e.g.tags[],meta.source)
- Endpoint:
POST /api/incidents - Purpose: Declare incidents with canonical
incident_idplus core metadata. - Example request:
{
"incident_id": "IR-2026-0001",
"title": "Potential ransomware activity on finance subnet",
"status": "declared",
"severity": "high",
"started_at": "2026-02-11T14:00:00Z",
"declared_by": "SOC Analyst",
"comms_channel": "Slack #ir-warroom",
"summary": "Initial declaration after correlated endpoint alerts.",
"metadata": {
"business_unit": "Finance",
"region": "US"
}
}- IOC enrichment supports live provider APIs with configurable keys from Settings.
- If no live provider is enabled or reachable, enrichment falls back to deterministic mock OSINT data.
- Database persistence is provided by PostgreSQL (see
.env.exampleanddocker-compose.yml). - Geo-IP lookups use the public
ipapi.coendpoint by default. - Security docs:
JWT.md(auth + DPoP binding),ASVS.md(ASVS Level 2 gap assessment),DATASTRUCTURE.md(DB schema + ERD)
- See
CHANGELOG.mdfor release history.