Last updated: 2026-06-12
This document tracks planned features, improvements, and technical debt for the Aegis platform. Items are organized by priority and grouped into phases.
High-priority items to make the existing platform fully functional end-to-end.
- Status: β Completed (2026-06-11)
- Priority: π΄ Critical
- Description: API Tokens tab in Settings page for managing agent authentication tokens.
- Implemented:
- Token creation dialog (name, project scope, expiry)
- Show plaintext token once after creation (with copy button + security warning)
- List tokens with prefix, name, project scope, expiry, status (active/expired/revoked)
- Revoke button per token
tokensApiclient (create, list, revoke)
- Status: β Completed (2026-06-11)
- Priority: π΄ Critical
- Description: Finding detail page displays agent-pushed metadata: CVE, CVSS score (color-coded), seen count, last seen at, verification status.
- Implemented:
- CVE display in sidebar details
- CVSS score with color indicator (red β₯9.0, orange β₯7.0, amber β₯4.0, blue <4.0)
seen_countdisplay ("Seen N times") when >1 for dedup visibilitylast_seen_attimestamp when different from created_atverifiedstatus added to findings list filter dropdown- Project filter dropdown on findings list page
- Status: β Completed (2026-06-12)
- Priority: π΄ Critical
- Description: Users can create new organizations from the sidebar org switcher. The backend existed but the UI had no dialog wired to it.
- Implemented:
- Create Organization dialog (name + auto-generated slug + slug preview)
- Slug validation (3-50 chars, lowercase alphanumeric + hyphens, reserved slug check)
- Live subdomain preview (
slug.aegis.io) - Error display for duplicate slugs, reserved names, and validation failures
- Backend: improved error handling (409 Conflict for reserved/duplicate slugs, plan allowlist validation)
- Org switcher "Create Organization" button wired to dialog
- Auto-switch to new org after creation
- "No Organization" empty state now clickable to open dialog
Port the Aegis scanning agent into the monorepo and connect it to the platform. This is the core value proposition β without the scanning engine, the platform is a dashboard with no data source.
- Status: π‘ In progress (agent binary complete, UI pending)
- Priority: π΄ Critical
- Description: Port the Aegis scanning agent from
local-harness/agents/aegis/intoagent/in the Aegis monorepo. The agent becomes a standalone Go binary and Docker image that scans codebases using AI-powered personas and pushes findings to the Aegis server via the existing agent ingest API. - Architecture:
- Monorepo layout:
agent/directory with its owngo.mod(separate Go module),Dockerfile, and binary - External dependency:
github.com/pixelvide/localharness/adk(public module) for the agent runtime - Binary name:
aegis(server binary is alreadyaegis-server, no conflict) - Docker image:
aegis-agent(separate fromaegis-server) - Docker Compose: Agent is NOT included in docker-compose.yml. Users run the agent binary or Docker image manually.
- Monorepo layout:
- Agent β Server communication:
- Agent generates a UUID v7 (time-sortable) as
scan_idat the start of each run - Agent registers the scan via
POST /api/v1/agent/scansbefore pushing findings - Every finding pushed includes this
scan_idfor correlation - Server groups findings by
scan_idfor display on the Scans page - Agent signals completion via
PATCH /api/v1/agent/scans/{id}β server computes severity summary from findings - Graceful shutdown: agent sends
{status: "failed"}on SIGINT/SIGTERM via signal handler
- Agent generates a UUID v7 (time-sortable) as
- Finding reporting via
report_findinghost tool:- Personas call
report_finding()directly with structured finding data - Reporter pushes to Aegis server via
POST /api/v1/agent/findingswith 3x exponential backoff retry - Findings always saved locally to
.aegis/findings.jsonregardless of server connectivity - No file scraping, hooks, or post-scan scanning β clean tool-based integration
- Personas call
- Configuration (layered resolution):
- CLI flags > env vars > config.yml (workspace) > config.yml (global)
AEGIS_BASE_URL/--report-base-url/reporting.base_urlβ server URLAEGIS_API_KEYβ env-var-only (no CLI flag to avoid process list / shell history leaks)AEGIS_PROJECT_ID/--project/reporting.project_idβ project UUID
- CLI interface:
# Push to server (API key always from env var) AEGIS_API_KEY=aegis_xxx \ aegis --report-base-url=https://acme.aegis.io --project=<uuid> sharingan # All via env vars (ideal for CI/Docker) AEGIS_BASE_URL=https://acme.aegis.io AEGIS_API_KEY=aegis_xxx AEGIS_PROJECT_ID=<uuid> \ aegis sharingan # Offline mode (local findings.json only) aegis sharingan # Docker docker run --rm \ -v /path/to/project:/workspace \ -e GEMINI_API_KEY=xxx \ -e AEGIS_API_KEY=aegis_xxx \ -e AEGIS_BASE_URL=https://acme.aegis.io \ -e AEGIS_PROJECT_ID=<uuid> \ aegis-agent sharingan
- Personas ported:
- ποΈ Sharingan β Full security audit & reconnaissance
- π§ͺ Senku β Supply chain & dependency analysis
- β‘ Killua β Targeted penetration testing
- Subagents ported:
exploit-writerβ Creates finding reports + working exploit PoC scriptsdeep-tracerβ Read-only deep analysis of specific files/components
- Completed (2026-06-12):
- β
Copied agent code from
local-harness/agents/aegis/βagent/ - β
Created
agent/go.mod(separate module with replace directive for local dev) - β
Added
ReportingConfigtoconfig.go(layered resolution with env vars) - β
Added
--report-base-urland--projectCLI flags tomain.go - β
Created
reporter.goβ HostTool-based reporter (report_findingtool) - β
UUID v7 scan correlation via
github.com/google/uuid - β
Created
agent/Dockerfile(golang:1.25-alpine build β ubuntu:24.04 runtime) - β
Created
agent/aegis-runDocker convenience script - β
Server:
scan_idadded toAgentFindingRequest(required field + validation) - β
Documentation:
docs/agent.md - β
Agent scan lifecycle:
POST /agent/scans(start) +PATCH /agent/scans/{id}(complete/fail) - β Agent signal handler (SIGINT/SIGTERM) sends scan failure on crash
- β Server computes scan summary from findings on completion
- β UI: Scans page β card-based layout with severity breakdown bars, stale scan detection
- β
Copied agent code from
- Remaining (deferred):
- UI: Agents page updated with setup/usage instructions (token + CLI command)
Exploitable security gaps that must be fixed before onboarding users. A security platform without rate limiting and account lockout is not credible.
- Status: Not started
- Priority: π΄ Critical
- Description: Rate limit auth endpoints (login, register, password reset, MFA) and agent ingest endpoints to prevent brute-force and abuse. Use Valkey for distributed counters.
- Requirements:
- Auth endpoints: 5 attempts per minute per IP for login/register, 3 per hour for password reset
- Agent endpoints: Per-org token bucket (free: 100 req/min, pro: 1000, enterprise: 10000)
- Global: 1000 req/min per IP across all endpoints
- Valkey-backed sliding window counters
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Resetresponse headers429 Too Many RequestswithRetry-Afterheader- Rate limit middleware in the request chain (before auth, for auth endpoints)
- Status: Not started
- Priority: π΄ Critical
- Description: Lock accounts after N consecutive failed login attempts to prevent brute-force password guessing. Currently there is no lockout β unlimited attempts are allowed.
- Requirements:
- Track failed login attempts per account in Valkey:
login_failures:{user_id}counter with TTL - After 5 consecutive failures β lock account for 15 minutes
- After 10 consecutive failures β lock account for 1 hour
- After 20 consecutive failures β lock account until manual unlock or password reset
- Reset counter on successful login
- Send email notification to user when account is locked
common.account_lockoutstable:user_id UUID PK,locked_until TIMESTAMPTZ,attempt_count INT,last_attempt_at TIMESTAMPTZ- Admin API to manually unlock accounts
- User-facing error: "Account temporarily locked. Try again in X minutes or reset your password."
- Track failed login attempts per account in Valkey:
- Status: Not started
- Priority: π΄ Critical
- Description: OTP codes (6 digits) can currently be guessed endlessly with no attempt limit. An attacker with a valid MFA token can try all 1,000,000 combinations.
- Requirements:
- Track MFA verification attempts per MFA token in Valkey:
mfa_attempts:{mfa_token}counter with TTL matching MFA token expiry - After 3 failed attempts β invalidate the MFA token entirely, force user to re-enter password
- After 5 failed attempts (across multiple MFA tokens) within 10 minutes β trigger account lockout
- Rate limit
POST /api/v1/auth/mfa/validateto 1 request per 2 seconds per IP - Rate limit
POST /api/v1/auth/mfa/send-email-otpto 1 request per 30 seconds per MFA token - Log all failed MFA attempts to audit log
- User-facing error: "Too many failed attempts. Please log in again."
- Track MFA verification attempts per MFA token in Valkey:
- Status: β Completed (2026-06-13)
- Priority: π΄ Critical
- Description: The
require_mfaorg-level feature flag is now enforced in TenantResolver middleware. When enabled, users without MFA are blocked from all org-scoped endpoints with 403 (E60003). - Implemented:
require_mfaflag seeded inProvisionOrgSchema()(provisioned=TRUE, enabled=FALSE)- Migration 12 seeds the flag for all existing org schemas
- TenantResolver middleware checks
require_mfaflag +user.MFAEnabledβ returns 403mfa_required_by_orgif enforcement is on and user has no MFA - Agent (Bearer token) requests are NOT affected β they use
TokenAuthmiddleware - UI:
OrgProviderdetectsmfa_required_by_orgerror and shows full-page interstitial with "Set Up MFA" button redirecting to base domain profile - Error code
errMFARequiredByOrg(E60003) already existed in middleware and API error constants
- Status: Not started
- Priority: π΄ Critical
- Description: Replace the current single long-lived JWT with a short-lived access token + long-lived refresh token pair. Refresh tokens are rotated on each use (one-time use), preventing replay attacks if a token is stolen.
- Requirements:
- Short-lived access token (15 min) + refresh token (7 days) pair
common.refresh_tokenstable:id UUID PK,user_id UUID,token_hash TEXT,family TEXT(rotation chain),expires_at TIMESTAMPTZ,revoked_at TIMESTAMPTZPOST /api/v1/auth/refreshendpoint: validate refresh token, issue new access+refresh pair, revoke old refresh token- Rotation detection: If a revoked refresh token is reused, revoke the entire token family (all descendants) β this indicates theft
- Update frontend
request()helper to auto-refresh on 401 - Graceful migration: existing sessions continue to work until expiry
- Status: Not started
- Priority: π‘ Medium
- Description: Users can currently use the platform with unverified email addresses. No handler or middleware gates access based on email verification status.
- Requirements:
- Add email verification check in TenantResolver or Auth middleware
- If user's primary email is unverified, return 403 with redirect to verification page
- Grace period: allow access for 24 hours after registration before enforcing
- Resend verification email endpoint (already exists)
- Status: Not started
- Priority: π‘ Medium
- Description: No React error boundaries exist β unhandled errors crash the entire app with a white screen.
- Requirements:
- Add top-level
ErrorBoundarycomponent wrapping the app - Add per-page error boundaries for graceful degradation
- Show user-friendly error message with "Reload" button
- Log errors to console (and later to server-side error tracking)
- Add top-level
- Status: Not started
- Priority: π‘ Medium
- Description: Currently relying solely on
SameSite=Laxcookies for CSRF protection. WhileSameSite=Laxprevents most CSRF attacks, it does not protect against top-level navigation attacks (GET-based state changes) and some browser edge cases. - Requirements:
- Generate a CSRF token on login and store in session (Valkey or DB)
- Return token via a
GET /api/v1/auth/csrfendpoint or as a response header - Frontend stores token in memory (not localStorage) and sends it as
X-CSRF-Tokenheader on all mutating requests (POST/PATCH/DELETE) - Server middleware validates
X-CSRF-Tokenmatches session token on all mutating requests - Exempt agent API (Bearer token auth) from CSRF checks β CSRF only applies to cookie-based auth
- Token rotation: regenerate on sensitive actions (password change, MFA toggle)
- Double-submit cookie pattern as fallback for non-SPA clients
Roles exist in the data model (owner, admin, member, viewer) and the middleware resolves them into context, but no handler checks the role. A viewer can currently delete findings, revoke tokens, and remove members.
- Status: Not started
- Priority: π΄ Critical
- Description: Implement a two-tier RBAC system with org-level roles (controlling org-wide access) and project-level roles (controlling per-project access). Inspired by Langfuse's RBAC model, adapted for Aegis's security scanning domain.
Aegis RBAC operates at two scopes:
- Org-level role β set in
common.org_members.role. Determines default access to all projects in the org and controls org-wide resources (members, feature flags, billing, audit logs). - Project-level role β set in
org_<uuid>.project_members. Overrides the org-level default for a specific project. Enables granting access to individual projects without org-wide visibility.
Resolution logic:
effective_access(user, project) =
project_role(user, project) β if explicitly assigned
?? org_role(user) β fallback to org-level default
- Org
Ownerβ gets Owner-level access to all projects by default (unless overridden) - Org
None+ ProjectMemberon "Backend API" β can only see that one project's findings/scans - Org
Member+ ProjectAdminon "Mobile App" β elevated access on that specific project
| Owner | Admin | Member | Viewer | None | |
|---|---|---|---|---|---|
| Organization | CRUD, delete, transfer | update | β | β | β |
| Org Members | CUD, read | CUD, read | read | β | β |
| Projects | create, delete | create | β | β | β |
| Org Feature Flags | CUD, read | CUD, read | β | β | β |
| Billing / Plan | CRUD | read | β | β | β |
| Audit Logs | read | read | β | β | β |
| API Tokens (org-wide) | CUD, read | CUD, read | β | β | β |
- Owner: Full control β org deletion, ownership transfer, billing, all resources.
- Admin: Manage members, projects, tokens, feature flags. Cannot delete org or transfer ownership.
- Member: Read-only view of org members. Primary access is through project-level scopes (see below).
- Viewer: No org-level scopes. Read-only access to projects they can see (all projects, since org role is the default floor).
- None: No org-wide access at all. User must be explicitly granted project-level roles to access anything. Useful for external contractors, auditors, or per-team scoping.
When a user has an org role but no explicit project role, the org role determines their default project-level access across all projects:
| Owner | Admin | Member | Viewer | None | |
|---|---|---|---|---|---|
| Project Settings | read, update, delete | read, update | read | read | β |
| Project Members | CUD, read | CUD, read | read | β | β |
| Findings | CUD, read | CUD, read | CUD, read | read | β |
| Scans | CUD, read, delete | CUD, read | create, read | read | β |
| Exploits | CUD, read | CUD, read | read | read | β |
| API Tokens (project-scoped) | CUD, read | CUD, read | read | β | β |
| Dashboard / Stats | read | read | read | read | β |
Key notes:
- Member can create scans and triage findings (CUD on findings = change status, add notes) but cannot delete scans or manage project members.
- Viewer is strictly read-only across all project resources.
- None gets zero access unless a project-level role is explicitly assigned.
Project-level roles allow overriding the org-level default for a specific project. The exact project roles and their scopes will be defined in a future iteration.
Expected roles: admin, member, viewer (no owner or none at project level β those are org-level concepts).
Expected storage: org_<uuid>.project_members table:
CREATE TABLE project_members (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES common.users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (project_id, user_id)
);Data model changes:
- Add
noneas a valid org role incommon.org_members.role - Create
org_<uuid>.project_memberstable (new table in org schema) - Update
ProvisionOrgSchema()to includeproject_memberstable - Add migration for existing org schemas
Backend (Go server):
- RBAC middleware or per-handler checks β extract user's org role from
common.org_members, check project role fromproject_membersif applicable, compute effective role - Scope-checking helper:
hasPermission(ctx, resource, action) boolβ used in each handler to gate access - Define permissions as constants:
PermFindingsRead,PermFindingsCUD,PermMembersRead,PermMembersCUD,PermProjectsCreate,PermOrgDelete, etc. - Map each role to its set of permissions (org-level and project-level separately)
- Return
403 Forbiddenwith{"error": "insufficient permissions"}when a user lacks the required scope
Frontend (React UI):
- Fetch user's effective role via
GET /api/v1/auth/me(org role) and project context - Expose role in
useAuth()oruseOrg()context - Hide/disable UI elements based on role (e.g., hide "Create Project" button for
member, hide "Delete" forviewer) - Role dropdown in member management (Settings β Members tab) with tooltip showing what each role can do (like Langfuse's pattern)
API changes:
GET /api/v1/membersβ include role in response (already done)PATCH /api/v1/members/{userId}β change a member's org role (new endpoint, admin+ only)GET /api/v1/projects/{slug}/membersβ list project members with roles (new)POST /api/v1/projects/{slug}/membersβ add project member with role (new)PATCH /api/v1/projects/{slug}/members/{userId}β change project role (new)DELETE /api/v1/projects/{slug}/members/{userId}β remove project member (new)
Infrastructure for per-org schema migrations and completing the feature flag system.
- Status: Not started
- Priority: π΄ Critical
- Description: Track a
schema_versionper org so tenants can be on different schema versions. This allows rolling upgrades where org A stays on v1 while org B is upgraded to v2. - Design:
- Add
schema_version INTEGER DEFAULT 1tocommon.organizations. - Each org schema migration records which version it applies. The migration runner checks the org's current version and only applies migrations above that version.
- Graduated rollout: New migrations can target specific orgs or org plans first (e.g., upgrade
enterpriseorgs to v2 first, thenpro, thenfree). - Rollback safety: Each migration version is immutable once applied. Rollback requires a separate "down" migration at a higher version number.
- Version-gated features: Org-level feature flags can reference a minimum schema version. If an org hasn't been migrated to the required version, the flag is force-disabled regardless of its setting.
- Add
- Changes needed:
- Add
schema_versioncolumn tocommon.organizations(migration) - Add
org_schema_migrationstable to each org schema to track per-org migration history - Update
ProvisionOrgSchema()to stamp the latest version - Create a migration runner that operates per-org (not globally)
- Admin API to check org version and trigger upgrades
- Feature-version coupling: Org feature flags include an optional
min_versionfield β the feature is auto-disabled if the org's schema version is below this threshold
- Add
- Status: Partially implemented
- Priority: π΄ Critical
- Description: The
org_feature_flagstable, store methods, and API endpoint exist, but onlyorg_wide_tokensis seeded. Need to seed all planned flags, enforceorg_wide_tokensin token creation, and build the settings UI. - What exists:
org_xxx.org_feature_flagstable (provisioned inProvisionOrgSchema)- Store interface:
IsOrgFeatureActive(),ListOrgFeatureFlags(),SetOrgFeatureEnabled() - API:
GET/PUT /api/v1/org-features
- What's missing:
- Seed additional flags:
require_mfa,api_access,scan_docker_mode,advanced_reports,webhooks,sso,custom_domain,ip_restrictions,email_domain_restriction,auto_join - Enforce
org_wide_tokensflag in token creation handler - UI: Settings page tab for org admins to view and toggle org-level features
- RBAC check on feature flag toggle endpoint (admin+ only, depends on Phase 4)
- Seed additional flags:
Audit logging is a cross-cutting concern that gets exponentially harder to retrofit. Every handler added without audit hooks is a handler that needs to be instrumented later.
- Status: Not started
- Priority: π΄ Critical
- Description: Log security-relevant actions (member invite/remove, scan create/delete, finding triage changes, feature flag changes, IP allowlist changes, login/logout, password changes, MFA changes) for compliance. Essential for a security platform.
- Changes needed:
audit_logtable in per-org schema:id UUID,actor_id UUID,action TEXT,resource_type TEXT,resource_id UUID,metadata JSONB,ip_address TEXT,user_agent TEXT,created_at TIMESTAMPTZcommon.auth_audit_logtable for auth events (login, logout, password changes, MFA changes) β not org-scoped- Store interface + postgres implementation
- Write audit entries from handlers (non-blocking via goroutines)
- UI page to browse and filter audit log (by actor, action, date range)
- Export to CSV/JSON for compliance reports
- Status: Partially implemented
- Priority: π‘ Medium
- Description: Track and display detailed usage information for API tokens β last used time, source IPs, request counts, and usage history. Currently
last_usedtimestamp is stored and updated on each token auth, but not displayed in the UI and no IP/usage history is recorded. - What exists:
last_used TIMESTAMPTZcolumn inorg_xxx.api_tokenstableUpdateTokenLastUsed()store method (called async on each token auth)last_usedfield in APIToken model and API response
- What's missing:
- Token usage log table:
org_xxx.api_token_usageβid UUID PK,token_id UUID,ip_address TEXT,user_agent TEXT,endpoint TEXT,status_code INT,created_at TIMESTAMPTZ - Last used IP: Add
last_used_ip TEXTcolumn toapi_tokenstable (updated alongsidelast_used) - IP history: Track unique IPs per token (derived from usage log). Display as a list of IPs with first/last seen timestamps.
- Usage count: Add
usage_count BIGINT DEFAULT 0column toapi_tokens(incremented on each use) - UI β Token list: Show "Last used" column (relative time, e.g., "2 hours ago") and "Last IP" in the token table
- UI β Token detail dialog/panel: Click a token row to see:
- Last used time + IP
- Total usage count
- Unique IP addresses (with first/last seen)
- Recent usage log (last 50 requests: timestamp, IP, endpoint, status)
- Backend changes:
- Update
TokenAuthmiddleware to log IP, user agent, endpoint toapi_token_usage(async, non-blocking) - Update
UpdateTokenLastUsed()to also setlast_used_ip - Increment
usage_counton each use - New store methods:
LogTokenUsage(),GetTokenUsageStats(),ListTokenUsageLog() - New API endpoints:
GET /api/v1/tokens/{id}/usage(admin+),GET /api/v1/projects/{projectId}/tokens/{id}/usage
- Update
- Retention: Auto-purge usage logs older than 90 days (configurable)
- Token usage log table:
- Status: Not started
- Priority: π‘ Medium
- Description: Allow orgs to restrict access to specific IP addresses or CIDR ranges. Controlled via the
ip_restrictionsorg-level feature flag. - Requirements:
org_xxx.ip_allowlisttable:id UUID PK,cidr TEXT NOT NULL,description TEXT,created_by UUID,created_at TIMESTAMPTZ- API endpoints:
GET/POST/DELETE /api/v1/ip-restrictions(admin+ role) - Middleware check: if
ip_restrictionsflag is enabled, validate request IP against allowlist before granting access - Applies to both user (cookie) and agent (bearer token) requests
- UI: Settings page tab for managing IP allowlist entries
- Support IPv4 and IPv6 CIDR notation
- Status: Not started
- Priority: π‘ Medium
- Description: Allow orgs to restrict which email domains can join. Controlled via the
email_domain_restrictionorg-level feature flag. - Requirements:
org_xxx.allowed_email_domainstable:domain TEXT PK,created_at TIMESTAMPTZ- When inviting a user or accepting a join request, validate email domain against the allowlist
- API endpoints:
GET/POST/DELETE /api/v1/email-domains(admin+ role) - UI: Settings page tab for managing allowed email domains
- Status: β Completed (2026-06-11)
- Priority: π’ Low
- Description: Users can view and revoke active sessions from Profile β Active Sessions tab. Sessions store browser, OS, device type (parsed from User-Agent server-side). Valkey cache layer reduces DB load on session revocation checks.
- Implemented:
common.user_sessionstable with JTI, IP, user agent, browser, OS, device type- Profile page: list sessions, revoke individual, revoke all others
- Valkey cache for session revocation checks (5-min TTL, graceful fallback to DB)
- Future:
- Org admin action: "Force logout all members" (revokes all sessions for users in the org)
Fill in the sidebar sections that exist as navigation items but have no implementation.
- Status: Not started
- Priority: π‘ Medium
- Description:
/reportsroute is in the sidebar nav but has no page. - Requirements:
- Generate PDF/HTML security reports for a scan
- Executive summary with severity charts
- Detailed findings list with remediation guidance
- Export to PDF
- Status: Not started
- Priority: π‘ Medium
- Description:
/analyticsroute is in the sidebar nav but has no page. - Requirements:
- Trends over time (findings opened vs closed)
- Severity distribution charts
- Mean time to remediation
- Per-scan comparison view
- Needs new API endpoints for time-series data
- Status: Not started
- Priority: π‘ Medium
- Description: The pagination envelope infrastructure is ready (
ResultInfo,parseResultInfo,writeList), but no SQL query actually paginates. All list endpoints return ALL rows. - Requirements:
- Add
LIMIT/OFFSETandCOUNT(*) OVER()to paginated SQL queries - Update
ListFindings,ListScans,ListProjects,ListAPITokens - Use
writeListwith populatedResultInfofor all list endpoints - Frontend: pagination controls on findings, scans, and projects pages
- Add
- Status: Partially implemented
- Priority: π‘ Medium
- Description: Projects exist as an entity and the
scanstable already has aproject_idcolumn. What's missing is the API support for filtering scans by project and the project detail page. - Changes needed:
- Update scan list API to filter by project
- Project detail page showing its scans and aggregate findings
- Agent: pass project context when pushing findings
- Status: Not started
- Priority: π’ Low
- Description: Per-project configuration (default persona, auto-scan schedule, notification preferences).
- Status: Not started
- Priority: π‘ Medium
- Description: Provide ready-to-use GitHub Action, GitLab CI template, and generic CLI examples for pushing findings from CI pipelines.
- Requirements:
- Example GitHub Action workflow YAML
- Example GitLab CI job config
curl-based examples for any CI system- Document token scoping per project
- Status: Not started
- Priority: π’ Low
- Description: Allow orgs to configure webhooks that fire when findings are created, status changes, or agents complete a scan pass. Controlled via the
webhooksorg-level feature flag. - Requirements:
org_xxx.webhookstable (URL, events, secret)- HMAC-signed POST to webhook URL
- Retry with exponential backoff
- Status: Not started
- Priority: π‘ Medium
- Description: Currently members can only be added if they already have an account. Need an invite-by-email flow where non-registered users receive an email invitation to join an org.
- Requirements:
common.org_invitationstable:id UUID PK,org_id UUID,email TEXT,role TEXT,invited_by UUID,token_hash TEXT,expires_at TIMESTAMPTZ,accepted_at TIMESTAMPTZPOST /api/v1/members/inviteβ send invitation email (admin+ role)POST /api/v1/auth/accept-inviteβ accept invitation (creates account if needed, adds to org)- Invitation email with accept link
- UI: invite dialog in Settings β Members tab
- Status: Not started
- Priority: π‘ Medium
- Description: Currently a single OpenAPI spec is served on all domains. Split into two specs served contextually.
- Requirements:
openapi-base.yamlβ auth, registration, password reset, MFA, user management endpointsopenapi-org.yamlβ findings, scans, projects, tokens, members, dashboard, agent ingest endpoints- Base domain (
aegis.io/api/v1/docs) β servesopenapi-base.yaml - Org subdomain (
acme.aegis.io/api/v1/docs) β servesopenapi-org.yaml - Swagger UI dynamically loads the correct spec based on domain
- Status: Not started
- Priority: π’ Low
- Description: Send scan completion and critical finding alerts to Slack or Microsoft Teams channels.
- Requirements:
org_xxx.notification_channelstable (type, webhook_url, events, enabled)- Slack incoming webhook integration
- Teams incoming webhook integration
- Configurable event filters (severity threshold, scan status changes)
- Status: Not started
- Priority: π’ Low
- Description: Allow enterprise orgs to configure SAML-based SSO. Controlled via the
ssoorg-level feature flag. - Requirements:
- SAML 2.0 SP implementation
org_xxx.sso_configtable (entity_id, sso_url, certificate, etc.)- Login flow: redirect to IdP β callback β create/link user β set session
- Support for Okta, Azure AD, Google Workspace, OneLogin
- Auto-provisioning of users from IdP attributes
- Status: Not started
- Priority: π‘ Medium
- Description: Detect and alert on potentially malicious login patterns: new device/location, impossible travel, brute-force attempts, and credential stuffing.
- Requirements:
- New location/device alert: Compare login IP + UA against user's session history. If new, send an email alert (partially done β login notification emails already sent on every login).
- Impossible travel detection: If two logins from geographically distant locations within a short time window, flag the newer session and notify the user.
- Brute-force detection: Track failed login attempts per IP and per account in Valkey. Lock out after N failures (e.g., 5 failures β 15-min lockout).
- Credential stuffing detection: Track failed login rate per IP across multiple accounts. If an IP fails against many different accounts, block the IP temporarily.
- GeoIP integration: Add MaxMind GeoLite2 (or ip-api) for IP-to-location resolution. Store country/city in
user_sessions. common.security_eventstable:id UUID,user_id UUID,event_type TEXT,ip TEXT,metadata JSONB,created_at TIMESTAMPTZ- Admin dashboard widget showing recent security events
- Org-level feature flag:
suspicious_activity_detection
- Status: Not started
- Priority: π‘ Medium
- Description: Strengthen password validation beyond minimum length. Check passwords against the HaveIBeenPwned breached passwords database (k-anonymity API β only sends a 5-char hash prefix, never the full password).
- Requirements:
- Integrate HIBP Pwned Passwords API (k-anonymity model:
GET https://api.pwnedpasswords.com/range/{prefix}) - Reject passwords that appear in known breaches during registration, password change, and password reset
- Configurable minimum complexity rules (uppercase, digit, special char) β enforce via org-level feature flag
strict_password_policy - User-facing error: "This password has appeared in a data breach. Please choose a different password."
- Integrate HIBP Pwned Passwords API (k-anonymity model:
- Status: Not started
- Priority: π‘ Medium
- Description: Bind sessions to the originating IP address and User-Agent. If a session cookie is used from a different IP or device, require re-authentication. This prevents stolen cookie replay attacks.
- Requirements:
- Store
ip_addressanduser_agenthash incommon.user_sessions(already stored β need enforcement) - On each authenticated request, compare current IP + UA against the session's stored values
- Strict mode (org flag
strict_session_binding): mismatch β session revoked, user forced to re-login - Soft mode (default): mismatch β log a security event + send email alert, but allow the request
- Handle legitimate IP changes (mobile networks, VPNs) gracefully β soft mode is the safe default
- Store
- Status: Not started
- Priority: π‘ Medium
- Description: Add a
Content-Security-Policyheader to all responses to mitigate XSS, clickjacking, and data injection attacks. - Requirements:
default-src 'self'β restrict all resources to same-origin by defaultscript-src 'self'β no inline scripts, no evalstyle-src 'self' 'unsafe-inline'β allow inline styles (needed for shadcn/ui)img-src 'self' data:β allow data URIs for avatarsconnect-src 'self'β restrict fetch/XHR to same-originframe-ancestors 'none'β prevent framing (supplements X-Frame-Options)- Add CSP header in
ServeHTTPalongside existing security headers - Consider
report-uri/report-tofor monitoring violations in production
- Status: Not started
- Priority: π‘ Medium
- Description: Harden auth cookies with
Secureflag and__Secure-prefix once HTTPS is enforced in production. Currently cookies useHttpOnly,SameSite=Lax, andPath=/but lackSecure: true(which would break local HTTP dev). - Requirements:
- Add
Secure: trueto all auth cookies (requires HTTPS) - Rename cookie from
aegis_tokento__Secure-aegis_token(browser enforces Secure + no Domain override) - Note:
__Host-prefix cannot be used because it disallowsDomainattribute, which is required for cross-subdomain cookie sharing - Consider making Secure flag configurable via env var for dev vs prod
- Add CSRF token protection for all mutating cookie-authenticated endpoints (see Phase 3 CSRF item)
- Add
- Status: Not started
- Priority: π’ Low
- Description: Prevent users from reusing recent passwords when changing or resetting their password. Stores hashed versions of previous passwords.
- Requirements:
common.password_historytable:id UUID PK,user_id UUID,password_hash TEXT,created_at TIMESTAMPTZ- On password change/reset, check new password against last N (default: 5) stored hashes via bcrypt
- Configurable depth via org-level feature flag
password_history_depth(default: 5) - Automatically prune history entries beyond the configured depth
- User-facing error: "You cannot reuse any of your last 5 passwords."
- Status: Not started
- Priority: π’ Low
- Description: Automatically add users to an org if their email domain matches a configured domain. Controlled via the
auto_joinorg-level feature flag. - Requirements:
org_xxx.auto_join_domainstable:domain TEXT PK,default_role TEXT DEFAULT 'member',created_at TIMESTAMPTZ- On user registration or first login, check if any org has an auto-join domain matching the user's email β if so, auto-add as member with the configured default role
- API endpoints:
GET/POST/DELETE /api/v1/auto-join-domains(owner role only) - UI: Settings page tab for configuring auto-join domains and default role
- Status: Not started
- Priority: π’ Low
- Description: The sidebar user menu has a "Notifications" item but nothing exists. Notify users when scans complete or critical findings are discovered.
- Status: β Completed (2026-06-11)
- Priority: π’ Low
- Description: Profile page with 4 tabs: Emails (multi-email management, verification), Password (change password), Authentication (multi-device MFA, recovery codes), Active Sessions (list, revoke, revoke all).
- Status: Not started
- Priority: π’ Low
- Description: The sidebar has a "Search" button at the bottom. Implement a global search / command palette (Cmd+K) to quickly navigate to scans, findings, or settings.
- Status: Not started
- Priority: π’ Low
- Description: The UI uses shadcn/ui which supports dark mode theming. Add a toggle in settings or the user menu.
- Status: Not started
- Priority: π’ Low
- Description: Enforce usage limits based on org plan (free, pro, enterprise).
- Requirements:
- Define limits per plan: max members, max projects, max scans/month, max API tokens, max findings retention
common.plan_limitstable or hardcoded config- Enforce in API handlers (return 402/403 when limit exceeded)
- UI banner showing usage vs. limit
- Status: Not started
- Priority: π’ Low
- Description: Allow orgs to configure how long findings and scan data are retained before automatic cleanup.
- Requirements:
data_retention_dayssetting in org feature flags or org settings table- Background job to purge expired data
- UI configuration in org settings
- Status: Not started
- Priority: π’ Low
- Description: Allow enterprise orgs to customize the UI with their logo, color scheme, and custom domain.
- Requirements:
org_xxx.brandingtable (logo_url, primary_color, accent_color)- Dynamic theme loading based on org context
- Custom email templates with org branding
- Status: Not started
- Priority: π’ Low
- Description: Allow orgs to choose their data region for compliance (GDPR, SOC2, etc.).
- Requirements:
data_regioncolumn oncommon.organizations- Route org queries to region-specific database
- Region selector during org creation
| Item | Description | Priority |
|---|---|---|
| β Completed | ||
{"error": "message"} β no machine-readable codes for programmatic handling |
β Completed | |
| β Completed | ||
| Error boundaries | No React error boundaries β unhandled errors crash the whole app | Phase 3 |
| Loading states | Some pages show raw empty states before data loads | π’ Low |
| Test suite | No unit tests or integration tests exist | π‘ Medium |
| OpenAPI spec update | Swagger spec needs agent ingest + token endpoints added; split base vs org specs | Phase 9 |
| Feature flag consistency | Migrate all feature-gated behavior to use the two-tier flag system (global + org) | Phase 5 |
- Status: β Completed (2026-06-12)
- Priority: π΄ Critical
- Description: All API endpoints now use a Cloudflare-style response envelope with consistent success/error formatting.
- Implemented:
- Standard success envelope:
{"success": true, "request_id": "req_...", "result": {...}} - Standard error envelope:
{"success": false, "request_id": "req_...", "errors": [{"type": "...", "code": "...", "ref": "E...", "message": "..."}]} - Helpers:
writeResult,writeResultMessage,writeList,writeMessage,writeApiError,writeValidationErrors - Pagination infrastructure:
ResultInfostruct,parseResultInfoquery parser,writeListfor paginated responses - Frontend:
ApiErrorclass, automatic envelope unwrapping inrequest(),requestList()helper - All ~250 handler call sites migrated from legacy
writeJSON/writeErrorto new helpers - Legacy
writeJSON/writeErrorfunctions deprecated (no callers remain)
- Standard success envelope:
- Not yet implemented:
- Store-level pagination (paginated SQL queries with
COUNT(*) OVER()) β deferred to Phase 7 go generatecodegen script for error registry β deferred
- Store-level pagination (paginated SQL queries with
- Status: β Completed (2026-06-12)
- Priority: π΄ Critical
- Description: All API errors now return structured error codes with type, code, reference ID, and message.
- Implemented:
- Master error registry:
errors.yaml(single source of truth) - Go error constants:
server/internal/api/errors.go(30+ pre-definedApiErrorconstants) - TypeScript error constants:
ui/src/lib/error-codes.gen.ts(matching frontend constants) - Error code categories:
auth_error(E1xxxx),token_error(E2xxxx),tenant_error(E3xxxx),resource_error(E4xxxx),validation_error(E5xxxx),permission_error(E6xxxx),rate_limit_error(E7xxxx),server_error(E9xxxx) WithMessage()andWithDetails()builder methods onApiError- Field-level validation errors via
writeValidationErrors()withFieldErrordetails - Frontend
ApiErrorclass with error code, type, ref, and message
- Master error registry:
- Status: β Completed (2026-06-12)
- Priority: π΄ Critical
- Description: Every API request now has a unique request ID propagated through logs, error responses, and response headers.
- Implemented:
- Request ID middleware (
server/internal/middleware/request_id.go) β generatesreq_<hex>IDs - Request ID context utilities (
server/internal/requestid/requestid.go) X-Request-IDresponse header on all responses- Request ID in all error response bodies (
request_idfield) - Request ID in all success response bodies (
request_idfield) - Logger integration:
request_idautomatically injected into allslogoutput - Accepts incoming
X-Request-IDfrom reverse proxies - Middleware is outermost in the chain (runs before auth, CORS, etc.)
- Request ID middleware (
| Feature | Date |
|---|---|
| Auth system (register, login, logout, JWT sessions) | Done |
| Multi-tenant architecture (per-org PostgreSQL schemas) | Done |
| Organization CRUD + auto-provisioning | Done |
| Schema migration system | Done |
| Feature flags (global) | Done |
| Finding CRUD + triage status | Done |
| Exploit storage + display | Done |
| Dashboard with aggregate stats | Done |
| Project CRUD | Done |
| Member invite/remove | Done |
| Login page | Done |
| Sidebar navigation + org/project switcher | Done |
| Finding detail page with markdown rendering | Done |
| Settings page (General + Members tabs) | Done |
| Agents page with persona cards | Done |
| Docker Compose single-image deployment | Done |
Health check endpoints (/healthz, /readyz) |
2026-06-11 |
OpenAPI 3.0 spec + Swagger UI (/api/v1/docs) |
2026-06-11 |
OpenTelemetry metrics + Prometheus exporter (/metrics) |
2026-06-11 |
| Agent Ingest API (push-based findings with fingerprint dedup) | 2026-06-11 |
| API token management (per-org, optional project scope) | 2026-06-11 |
| Bearer token auth middleware + subdomain org resolution | 2026-06-11 |
| CVE/CVSS scoring on findings | 2026-06-11 |
| Finding verification loop (agent confirms fixes) | 2026-06-11 |
| Reserved slug blacklist for org names | 2026-06-11 |
| Custom domain support in organizations | 2026-06-11 |
| SMTP email service (MailDev in dev, configurable in prod) | 2026-06-11 |
| Password reset via email (forgot/reset/change password) | 2026-06-11 |
| TOTP-based Multi-Factor Authentication (MFA) | 2026-06-11 |
| MFA recovery codes (10 one-time bcrypt-hashed codes) | 2026-06-11 |
| Pre-commit hook for Go/TypeScript linting | 2026-06-11 |
| Settings β Security tab (password change + MFA setup) | 2026-06-11 |
| Email verification (send + verify via token) | 2026-06-11 |
| Profile page (account security, email verification) | 2026-06-11 |
| Multi-device MFA (TOTP + email OTP, device CRUD) | 2026-06-11 |
| Multi-email management (add, verify, set primary, remove) | 2026-06-11 |
| Session tracking & management (JTI, list, revoke) | 2026-06-11 |
| Profile page redesign (3 tabs: Emails, Auth, Sessions) | 2026-06-11 |
| MFA login flow with device selector | 2026-06-11 |
| Recovery code regeneration (password-protected) | 2026-06-11 |
| Password change notification emails | 2026-06-11 |
| Login notification emails (browser, OS, IP, device) | 2026-06-11 |
| Rich session data (server-side UA parsing: browser, OS, device type) | 2026-06-11 |
| Valkey cache layer (session revocation caching, graceful fallback) | 2026-06-11 |
| Profile page redesign (4 tabs: Emails, Password, Auth, Sessions) | 2026-06-11 |
| Token Management UI (Settings β API Tokens tab) | 2026-06-11 |
Projects page (/projects with create dialog) |
2026-06-11 |
| Finding detail enhancements (CVE, CVSS, seen_count, last_seen_at) | 2026-06-11 |
| Findings filters (verified status, project filter) | 2026-06-11 |
| Base domain auth restriction (auth only on base domain, cookie domain scoping, subdomain redirect) | 2026-06-11 |
| Org Creation UI (create org dialog, slug validation, auto-switch) | 2026-06-12 |
Standardized API response envelope (Cloudflare-style success/result/errors format) |
2026-06-12 |
Structured error codes (30+ codes across 8 categories with errors.yaml registry) |
2026-06-12 |
Request trace IDs (req_<hex> on every request, in logs + headers + response bodies) |
2026-06-12 |
| Full handler migration (~250 call sites from legacy to new envelope/error format) | 2026-06-12 |
MFA Enforcement (require_mfa org flag β partial: data model + UI done) |
2026-06-11 |
require_mfa enforcement in TenantResolver middleware + MFA interstitial UI |
2026-06-13 |