Security Engineering
Security Model & Controls
Engineering documentation of how OmniSentient protects your repositories and data. Every claim is sourced from the running codebase. Gaps are disclosed as gaps, not hidden.
Authentication
GitHub OAuth → server-side JWT
Session storage
HttpOnly cookie, never JS-visible
Data isolation
org_id-scoped queries, server-side
Crypto in transit
TLS via Vercel Edge, HSTS preloaded
- Your GitHub repository code — read-only, scoped only to repositories you explicitly install the app on
- Your GitHub profile — username, email, org membership (used for authentication and billing)
- Your repository metadata — branch names, commit SHAs, pull request titles (for scan context)
- Raw source is stored only if you switch on Code Intelligence for a repository. It is indexed as verbatim chunks in
code_embeddings, deleted on a 12-month schedule, and removed immediately when the repository is deleted. Automated remediation alone reads manifests, not source.
01 — Architecture
Security Architecture
Four trust zones with explicit data-flow direction. The application server is the sole trust authority — the browser and external services hold no elevated credentials.
↑ receives: JWT in cookie
sends: HTTP requests + cookie
SUPABASE_KEY is explicitly excluded from the Jinja2 template context processor. A prior version injected it site-wide, which would have exposed a full-access database credential to any rendered page. The exclusion is enforced in create_app.py.
02 — Authentication
Authentication & Session Model
GitHub OAuth 2.0 with server-side-only code exchange. The client never receives a GitHub access token.
OAuth Flow
nonce.timestamp.HMAC-SHA256. Short-lived HttpOnly nonce cookie binds state to this browser. Works across serverless instances — no shared session storage needed.user:email read:user read:org — minimum required. allow_signup=false — only existing GitHub accounts can proceed. Redirect URI from BASE_URL env, not request headers.next redirect param validated as same-origin path — full-URL accepted only if hostname matches BASE_URL.https://github.com/login/oauth/access_token). Access token used server-side to fetch profile, then discarded. Never sent to browser.sub, github_user, org_id, iat, exp, nbf, iss, aud, jti. Cookie: HttpOnly, Secure, SameSite=Lax, name=omni_session.JWT Verification Chain
Session Properties
| Property | Value | Enforced in |
|---|---|---|
| Absolute lifetime | 12 hours (JWT_EXPIRY_SECONDS env) | jwt_service.py |
| Idle timeout | 30 minutes (last_seen claim) | jwt_service.py |
| HttpOnly | Yes — JavaScript cannot access the cookie value | set_session_cookie_flask() |
| Secure flag | Set on HTTPS; relaxed on local HTTP dev only | _cookie_secure() |
| SameSite | Lax — blocks cross-site form POST CSRF | set_session_cookie_flask() |
| Token revocation | DB-backed per jti + org_id — immediate invalidation | jwt_service.verify_jwt() |
| Algorithm | HS256 — custom implementation, no PyJWT library | jwt_service.py |
jwt_service.py but not yet implemented. The __Host- cookie prefix (prevents domain-level injection on shared hosts) is noted in source as recommended but not active.
Identity Resolution
Wave-4 audit (June 2026) found five parallel identity extractors with divergent logic — any one could be exploited while another was hardened. All were deleted and replaced with a single entry point: services.auth.identity.get_current_user(). All code is required to call this function. No alternative resolution paths exist.
developer cannot assign admin. Role data is stored in the database; client-supplied role values in requests are never trusted.
03 — Authorization
Authorization & Access Control
The backend never trusts client-supplied org, project, or role values. All access is validated against the database before data is returned.
Role Hierarchy
| Role | Capabilities | Who assigns |
|---|---|---|
| auditor | Read-only forensic audit ledger and compliance export | admin / owner |
| viewer | Read-only dashboard — no write or scan trigger | admin / owner |
| developer | Trigger scans, view findings, create pull requests | admin / owner |
| security_lead | Security configuration, incident management, scan policy | admin / owner |
| admin | Billing, team management, integration config, log deletion | owner only |
| owner | Full access — org-level controls, member removal | GitHub App installer |
Enforcement Principles
validate_project_access() queries the database to confirm the requesting user holds at least the required role for the specific project UUID. Client-supplied org/project are treated as unverified claims until DB-confirmed. Input parameters validated against ^[A-Za-z0-9_-]{1,64}$ before any database operation.
Cross-org access returns 403, not 404 — org existence is not disclosed. All 403 responses return the same generic message — no timing-based enumeration possible via response time.
The dev-bypass route (/auth/dev-login) is gated on settings.is_local_dev: requires ENV=development AND absence of VERCEL_ENV. Vercel preview deployments are publicly reachable but classified as non-local — they cannot trigger the bypass.
04 — Data Protection
Data Protection
What Data We Store
| Data type | Stored? | Retention | Encrypted at rest? |
|---|---|---|---|
| GitHub user ID + username | Yes — required for auth | Until account deletion | Platform (Supabase AES-256) |
| Org membership + role | Yes — required for RBAC | Until revoked | Platform (Supabase AES-256) |
| Scan findings + results | Yes — core product feature | Configurable per org | Platform (Supabase AES-256) |
| Audit event log | Yes — append-only | Retained per org policy | Platform (Supabase AES-256) |
| Raw repository source code | No — not stored | Not applicable | Not applicable |
| GitHub access tokens | No — discarded after OAuth | Not applicable | Not applicable |
| JWT session tokens | No — stateless; jti only | jti revocation table | Platform (Supabase AES-256) |
Encryption in Transit
All traffic is TLS-only via the Vercel Edge network. The application sets Strict-Transport-Security: max-age=31536000; includeSubDomains; preload on all non-local-dev responses, including preview deployments. A prior version applied HSTS in production only — preview deployments were left without it.
Encryption at Rest
Application-layer encryption is not implemented. Database-level encryption is provided by Supabase's platform (PostgreSQL on AWS with AES-256 at rest). All data is stored in a single Supabase project.
Secret Management
| Secret | Purpose | If absent in production |
|---|---|---|
| JWT_SECRET | HMAC signing key for session tokens | CRITICAL log · startup proceeds with insecure fallback (local dev guard prevents this in prod) |
| SECRET_KEY | Flask session signing | Startup validation fails — app does not start |
| SUPABASE_KEY | Service-role DB access — server-side only | All database operations fail |
| GITHUB_WEBHOOK_SECRET | HMAC verification for GitHub webhooks | All webhooks return 401 |
| INTERNAL_JOB_TOKEN | Worker-to-worker auth | Fail-closed: all internal requests rejected |
Credential Scrubbing in Logs
utils/security.py:mask_sensitive_data() applies regex patterns to all log output before writing, redacting GitHub tokens (ghp_*, github_pat_*), bearer tokens, AWS access keys, Slack tokens, Stripe keys, PEM private key headers, and generic api_key/secret patterns. Prompts sent to Gemini AI are scrubbed via SecurityUtilities.scrub_log() before leaving the application boundary.
Repository Access Scope
GitHub App installation tokens are scoped to the specific repositories selected during installation — not the user's entire GitHub account. Revoking the GitHub App installation immediately removes access at the GitHub API level, with no action required from OmniSentient.
05 — Application Security
Application Security Controls
Security Headers
| Header | Value | Status |
|---|---|---|
| Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | Implemented |
| Content-Security-Policy | Per-request nonce (secrets.token_hex(16)). Explicit CDN allowlist. Contains unsafe-inline + unsafe-eval — see limitation below. | Partial |
| X-Content-Type-Options | nosniff | Implemented |
| X-Frame-Options | SAMEORIGIN | Implemented |
| Referrer-Policy | strict-origin-when-cross-origin | Implemented |
| Cache-Control | no-store on /api/* and /dashboard/* | Implemented |
| Permissions-Policy | Not configured | Not implemented |
unsafe-inline + unsafe-eval in script-src means browsers use these permissions for scripts without nonces instead of blocking them. The per-request nonce is applied to known inline scripts, but the fallback weakens the protection. Strict CSP (nonce-only) requires a full inline-script audit and is on the roadmap.Webhook Signature Verification
GitHub webhook payloads: HMAC-SHA256 against GITHUB_WEBHOOK_SECRET before any payload parsing. Slack interactions: verify_slack_signature() before any field is read. Both are fail-closed — invalid signature = 401, no further processing. Webhook idempotency: DB unique constraint on delivery_id; duplicates return 202 without reprocessing, and quota is charged once only.
Internal Service Authentication
Worker-to-worker calls supply X-Internal-Job-Token header, validated via hmac.compare_digest (constant-time, timing-safe). INTERNAL_JOB_TOKEN supports comma-separated values for zero-downtime rotation. Fail-closed: an absent header or unset environment variable is always a rejection.
Application Posture Control
SECURITY_MODE=lockdown blocks all HTTP write operations (POST/PUT/PATCH/DELETE) at the application entry point — no redeploy needed. SystemPostureController reads from the governance store at runtime. /static/reports returns 404 unconditionally — compliance reports are never served from a static path.
Prompt Injection Detection
utils/security.py:is_prompt_injection() applies regex patterns against common jailbreak phrases before LLM prompt construction. This is a detection layer, not a complete defense — adversarial bypass remains possible. No semantic sandboxing or output filtering for LLM-generated content is implemented.
Known Missing Controls
| Control | Status | Real-world impact |
|---|---|---|
| Rate limiting | Not implemented | No application-layer throttle. An authenticated user could exhaust expensive AI scan or dependency check quotas without back-pressure. Vercel Edge may apply platform-level network limits. |
| CORS headers | Not implemented | No explicit CORS policy. Browser same-origin policy applies for browser clients. Server-to-server API callers are not affected by CORS. |
| Input validation library | Not implemented | No schema-level enforcement (Pydantic, Marshmallow). Validation is manual per-endpoint — inconsistency risk as the API grows. |
| CSRF token on API writes | Partial | SameSite=Lax provides partial protection. CSRF token exists in the Flask session context but is not validated against API write requests. |
06 — Infrastructure
Infrastructure & Deployment
| Property | Implementation |
|---|---|
| Hosting provider | Vercel — serverless Python functions + Edge Network |
| TLS | Managed by Vercel — automatic cert provisioning, renewal, and rotation |
| Environment isolation | Production, preview, and development environments are fully isolated by Vercel. Security headers apply identically to preview and production. |
| Secret management | Environment variables via Vercel project settings, scoped per environment. Never in source control. |
| Startup secret validation | settings.validate() runs at cold-start. Missing required secrets log CRITICAL. App proceeds but affected features will fail safely. |
| Mock endpoints | Gated on is_local_dev AND DEBUG=true. is_local_dev requires ENV=development AND no VERCEL_ENV set. A stray DEBUG=true on Vercel cannot expose them. |
| Proxy header handling | ProxyFix(x_for=1, x_proto=1, x_host=1) — trusts first X-Forwarded-For entry only. IPs validated via ipaddress.ip_address() before use. |
| Third-party dependencies | Python packages installed from PyPI at build time. No private registries. Dependency pinning via requirements.txt. |
Observability & Audit
All logs are structured JSON with per-request context propagated via Python contextvars (thread-safe and async-safe): request_id, tenant_id, user_id, role, path, method. Security events use log_security_event() and are tagged security_event: true for filtering. X-Request-ID is echoed in every response to enable trace correlation with client-side error reports.
An append-only audit ledger (audit_logs table) records organizational events with timestamp, actor identity, event type, and correlation ID. Every row carries the SHA-256 hash of the row before it, written by a Postgres trigger, so altering or removing a row breaks the chain from that point on and the break is detectable by anyone holding an earlier export. Users with the auditor role can query this via /api/auditor/* with time-range and correlation-ID filtering.
incidents, incident_events, incident_notifications,
which constrains other callers rather than the platform itself. The guarantee
is detection, not prevention — verify it yourself with the
verification guide and the
published signing key.
SECURITY_MODE=lockdown) and access the audit ledger immediately without waiting for OmniSentient support. For platform-level incidents, use the responsible disclosure process below.
07 — Threat Model
Threat Model
Realistic threats for a GitHub App processing code at scale. Mitigations reflect actual code. Gaps are documented, not hidden. Click any card to expand the technical residual risk.
unsafe-inline and unsafe-eval in CSP reduce nonce effectiveness — browsers use these as fallback rather than blocking unnonce'd scripts. Upgrade to nonce-only CSP closes this. The __Host- cookie prefix (prevents domain injection on shared hosts) is not active — not applicable on Vercel's isolated hosting but worth adding.get_current_user(). No alternative resolution path exists. Timing-safe 403 prevents org enumeration via response-time differences.nonce.timestamp.HMAC-SHA256(JWT_SECRET, nonce+timestamp). TTL 30 minutes. Works across all serverless instances — no shared session storage required. Requires JWT_SECRET to be consistent across instances (set via Vercel env). If absent: CRITICAL logged, insecure fallback only possible in local dev (Vercel guard blocks it in deployed environments).delivery_id prevents replay. Quota charged on first delivery only.08 — Lifecycle
Security Lifecycle
Security Review History
security_event: true in structured JSON logs and queryable via the audit ledger API.Dependency Scanning
OmniSentient includes a DependencyGuardian class that scans monitored repositories' dependencies against the OSV.dev vulnerability database. This is a feature we provide to customers — it does not scan OmniSentient's own Python dependencies.
Patch Response
OmniSentient does not publish a formal SLA for security patch response. As a general operating principle:
- Critical severity (active exploit, data exposure): target same-day investigation and 48-hour patch
- High severity: target 7-day patch
- Medium/low severity: addressed in next scheduled release
09 — Roadmap
Security Roadmap
Documented gaps with engineering priorities. These are not commitments — they reflect current engineering judgement. This list is updated with each wave audit.
| Item | Gap addressed | Priority | Target |
|---|---|---|---|
| API rate limiting | Per-user/IP sliding window on expensive endpoints (AI scans, dependency checks). Planned: Redis-backed. | High | Wave 5 |
| Strict CSP (nonce-only) | Remove unsafe-inline / unsafe-eval. Requires full inline-script audit to add nonces. | High | Wave 5 |
| RS256 JWT algorithm | Asymmetric keys — public key verification without distributing the signing secret. Upgrade path in jwt_service.py. | Medium | Wave 6 |
| CSRF token validation on API writes | X-CSRF-Token header checked on state-changing API requests to complement SameSite=Lax. | Medium | Wave 6 |
__Host- cookie prefix | Prevents domain-level cookie injection. Pre-conditions already met (HTTPS + no Domain attr). Path noted in source. | Medium | Wave 6 |
| Self-dependency SBOM | Scan OmniSentient's own Python dependencies against CVE databases. Publish SBOM. | Medium | Wave 6 |
| Permissions-Policy header | Restrict unused browser APIs (camera, microphone, geolocation, payment). | Low | Wave 7 |
| Input validation library | Replace manual per-endpoint validation with Pydantic schema enforcement for consistent error handling. | Low | Wave 7 |
| Subresource Integrity (SRI) | integrity attributes on CDN-loaded scripts/styles to prevent CDN-level supply chain attacks. | Low | Wave 7 |
10 — Disclosure
Report a Vulnerability
If you discover a security issue, please report it responsibly before public disclosure. We take reports seriously and will keep you informed.
Expected Response Timeline
Acknowledgement
< 48 hours
Status updates
every 7 business days
Public disclosure
within 90 days
Patch (critical)
< 48 hours
Patch (high)
< 7 days
These are operational targets, not contractual SLAs. We will communicate if circumstances require longer timelines.
What reviewers ask first
-
Where does our code go?
Dependency manifests only, by default.
Automated remediation reads manifests, not source. Source is read and stored only if you switch on Code Intelligence, per repository.
Privacy Policy → -
Who else touches it?
Four subprocessors, all named.
Supabase, Vercel, Google Cloud and GitHub. What each one receives, and where it sits, is listed rather than summarised.
Subprocessor Disclosure → -
What happens if you are breached?
72 Hours to notification.
4 Hours to acknowledgement, 14 business days to a published root-cause report.
Security Policy → -
Can I prove any of this myself?
Yes, offline, without asking us.
Every forensic export is Ed25519-signed. The public key is published and the verification runs in seven lines.
Verification Guide →
Documents
-
v1.2 · August 31, 2026
-
GDPR Article 28
-
Who receives what
-
Check it yourself
-
provenance_public_key — verify any signed export
Scope
✓ In scope
- Authentication and authorization bypass
- Cross-tenant data exposure
- Remote code execution
- SQL injection or database access
- Session token theft or forgery
- Security header bypass
- Server-side request forgery (SSRF)
- Secrets or credentials exposed in responses
✗ Out of scope
- Denial of service via high request volume
- Issues requiring physical device access
- Social engineering of OmniSentient staff
- Self-XSS (requires a user to attack themselves)
- Clickjacking on low-sensitivity pages
- Missing security headers on marketing pages
- Best-practice recommendations without demonstrated impact
How to Report
Do not open a public GitHub issue — this exposes the issue to all users before a fix is available.
- Describe the vulnerability: what it is, the affected component, how to reproduce it step by step, and the potential impact.
- Include evidence: URL, endpoint, or code path; HTTP request/response if applicable; proof-of-concept code if you have one.
- Do not exploit live user data beyond what is necessary to demonstrate the vulnerability.
- Submit via GitHub's Security → Report a vulnerability tab on the OmniSentient repository, or via the email in
SECURITY.mdin the repository root.
We will acknowledge within 72 hours and work with you on a coordinated disclosure timeline. We ask for a reasonable embargo before public disclosure.
OmniSentient does not currently operate a paid bug bounty program. We will credit researchers who report valid vulnerabilities with their consent.