Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
57b460f
fix(auth): respect tenant email-verification rule for new users
0xmanhnv Apr 9, 2026
d1f851b
fix(security,perf): close suspension gaps and merge login queries
0xmanhnv Apr 9, 2026
b677c21
fix(perf): batch role enrichment in member list (kill N+1)
0xmanhnv Apr 9, 2026
a4fb769
fix(owners): user owners were silently filtered out by tenant scope
0xmanhnv Apr 9, 2026
6e4ce0b
fix(perf): collapse GetMemberStats from 3 round trips to 1
0xmanhnv Apr 9, 2026
ea1ad84
fix(perf): always paginate /tenants/{t}/members?include=user
0xmanhnv Apr 9, 2026
fb8d507
fix(perf): Redis-cached GetMembership in middleware (kill per-request…
0xmanhnv Apr 9, 2026
cf33f15
feat(pentest): campaign RBAC, attachment system, S3 storage, activity…
0xmanhnv Apr 11, 2026
09b12b3
feat(pentest): CSV export for campaign findings
0xmanhnv Apr 11, 2026
963de73
fix(pentest): support legacy nested metadata format for seed data
0xmanhnv Apr 11, 2026
f6bb719
fix(pentest): comments display, view details button
0xmanhnv Apr 11, 2026
8de541b
chore: remove uploaded files from git, add data/ to .gitignore
0xmanhnv Apr 11, 2026
aff2d01
feat(pentest): full CSV export, finding number migration
0xmanhnv Apr 11, 2026
1417271
feat(pentest): Burp XML + CSV finding import
0xmanhnv Apr 11, 2026
33bb465
feat(pentest): report generation, campaign dashboard, OWASP checklist…
0xmanhnv Apr 12, 2026
d3278e6
feat(ctem): attack simulation, threat actor, report schedule modules
0xmanhnv Apr 12, 2026
ddb86a8
feat(assets): IP-hostname correlation engine + control test repository
0xmanhnv Apr 13, 2026
f174d69
feat(assets): lifecycle automation + test fixes for upsert behavior
0xmanhnv Apr 13, 2026
59c8f16
fix: add getters and constructor for ThreatActorCVE (staticcheck U1000)
0xmanhnv Apr 13, 2026
18603e4
feat(assets): add category mapping (33 types → 9 categories)
0xmanhnv Apr 13, 2026
c6b2eff
feat(remediation): campaign management — full CRUD + lifecycle
0xmanhnv Apr 13, 2026
c1991ce
feat(threat-intel): Asynq task definitions for EPSS/KEV daily refresh…
0xmanhnv Apr 13, 2026
35a279e
feat(dashboard): MTTR metrics + risk velocity trending
0xmanhnv Apr 13, 2026
c870dd5
feat(dashboard): expose MTTR + risk velocity via REST API
0xmanhnv Apr 13, 2026
a0b2b34
feat(scoping): business units + crown jewels
0xmanhnv Apr 13, 2026
83b0c13
feat(validation): verification scan workflow template + status_change…
0xmanhnv Apr 13, 2026
9379b35
feat(assets): crown jewel endpoint + SaveAsset method
0xmanhnv Apr 13, 2026
dcd81b0
data: seed network assets for vndirect tenant
0xmanhnv Apr 13, 2026
b9ecf15
feat: add properties JSONB + group_type to asset_groups
0xmanhnv Apr 13, 2026
dbca721
feat(assets): sub_type column + type aliasing + category fixes
0xmanhnv Apr 13, 2026
7fb7b6c
fix: 4 critical bugs in sub_type implementation + tests
0xmanhnv Apr 13, 2026
6f60b10
fix(migration): add new types to asset_types master table before cons…
0xmanhnv Apr 13, 2026
dc3f16b
fix(migration): Phase 1 only — add sub_type column WITHOUT changing a…
0xmanhnv Apr 13, 2026
53c68a3
feat(assets): crown jewel + sub_type filter params
0xmanhnv Apr 13, 2026
f6db91c
feat(threat-intel): EPSS + KEV HTTP fetch implementation
0xmanhnv Apr 13, 2026
b2cb4a0
feat(migration): consolidate legacy DB types (ip→ip_address, port→ser…
0xmanhnv Apr 13, 2026
e18e4b5
feat(migration): Phase 3 — consolidate asset_type values (33→15 core …
0xmanhnv Apr 13, 2026
4b677b0
feat(api): add by_sub_type to dashboard asset stats
0xmanhnv Apr 13, 2026
f79026e
fix(api): add bySubType to GetAllStats batched query
0xmanhnv Apr 13, 2026
fdc8663
feat(assets): add property facets API, server-side JSONB filtering, s…
0xmanhnv Apr 13, 2026
e568d70
feat(assets): auto-promote properties to columns on ingest
0xmanhnv Apr 14, 2026
9a2abc0
test: add unit tests + integration test for asset consolidation
0xmanhnv Apr 14, 2026
a16f003
fix: remove 22 unused argIdx++ assignments (CodeQL warnings)
0xmanhnv Apr 14, 2026
d0a0dc6
Merge branch 'main' into develop
0xmanhnv Apr 14, 2026
88a4542
feat(migration): 000132 — cleanup asset modules after type consolidation
0xmanhnv Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ cmd/server/server

vendor
go.work
go.work.sum
go.work.sumdata/
33 changes: 32 additions & 1 deletion cmd/server/handlers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package main

import (
"database/sql"

"github.com/openctemio/api/internal/app"
"github.com/openctemio/api/pkg/crypto"
"github.com/openctemio/api/internal/config"
"github.com/openctemio/api/internal/infra/http/handler"
"github.com/openctemio/api/internal/infra/http/middleware"
Expand Down Expand Up @@ -124,12 +127,31 @@ func NewHandlers(deps *HandlerDeps) routes.Handlers {
SLA: handler.NewSLAHandler(svc.SLA, v, log),

// Pentest Campaign Management
Pentest: handler.NewPentestHandler(svc.Pentest, repos.User, log),
Pentest: func() *handler.PentestHandler {
h := handler.NewPentestHandler(svc.Pentest, repos.User, log)
h.SetImportService(app.NewFindingImportService(repos.Finding, log))
return h
}(),
PentestCampaignRoleQry: repos.PentestCampaignMember,

// File Attachments (shared across pentest/retest/campaign)
Attachment: newAttachmentHandlerWithAccessCheck(svc.Attachment, svc.Pentest, deps.DB.DB, svc.Encryptor, log),

// Compliance Framework Management
Compliance: handler.NewComplianceHandler(svc.Compliance, log),

// Attack Simulation & Control Testing
Simulation: handler.NewSimulationHandler(svc.Simulation, log),

// Threat Actor Intelligence
ThreatActor: handler.NewThreatActorHandler(svc.ThreatActor, log),

// Remediation Campaigns
RemediationCampaign: handler.NewRemediationCampaignHandler(svc.RemediationCampaign, log),

// Business Units
BusinessUnit: handler.NewBusinessUnitHandler(svc.BusinessUnit, log),

// API Keys & Webhooks
APIKey: handler.NewAPIKeyHandler(svc.APIKey, v, log),
Webhook: handler.NewWebhookHandler(svc.Webhook, v, log),
Expand Down Expand Up @@ -242,3 +264,12 @@ func newAgentHandlerWithTemplates(

return h
}

// newAttachmentHandlerWithAccessCheck creates an AttachmentHandler with campaign
// membership verification for finding-scoped attachments.
func newAttachmentHandlerWithAccessCheck(attachSvc *app.AttachmentService, pentestSvc *app.PentestService, db *sql.DB, enc crypto.Encryptor, log *logger.Logger) *handler.AttachmentHandler {
h := handler.NewAttachmentHandler(attachSvc, log)
h.SetAccessChecker(pentestSvc)
h.SetStorageResolver(app.NewSettingsStorageResolver(db, enc, log))
return h
}
6 changes: 5 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ func run() int {
app.WithEmailEnqueuer(emailEnqueuer),
)
services.Tenant.SetPermissionServices(services.PermCache, services.PermVersion)
// Re-wire session service after rebuilding the tenant service —
// the constructor above replaces services.Tenant, dropping the
// SetSessionService call from initServices().
services.Tenant.SetSessionService(services.Session)

// Wire AI triage job enqueuer if service is enabled
if services.AITriage != nil {
Expand Down Expand Up @@ -222,7 +226,7 @@ func run() int {
}

server := http.NewServer(cfg, log)
routes.Register(server.Router(), handlers, cfg, log, authCfg, repos.Tenant, services.User)
routes.Register(server.Router(), handlers, cfg, log, authCfg, repos.Tenant, services.User, services.MembershipCache)

// Handle --routes flag
if *showRoutes {
Expand Down
32 changes: 32 additions & 0 deletions cmd/server/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,28 @@ type Repositories struct {
PentestTemplate *postgres.PentestTemplateRepository
PentestReport *postgres.PentestReportRepository

// Attachments (file upload metadata)
Attachment *postgres.AttachmentRepository

// Compliance
ComplianceFramework *postgres.ComplianceFrameworkRepository
ComplianceControl *postgres.ComplianceControlRepository
ComplianceAssessment *postgres.ComplianceAssessmentRepository
ComplianceMapping *postgres.ComplianceMappingRepository

// Attack Simulation & Control Testing
Simulation *postgres.SimulationRepository
ControlTest *postgres.ControlTestRepository

// Threat Actor Intelligence
ThreatActor *postgres.ThreatActorRepository

// Remediation Campaigns
RemediationCampaign *postgres.RemediationCampaignRepository

// Business Units
BusinessUnit *postgres.BusinessUnitRepository

// SLA & Integration
SLA *postgres.SLAPolicyRepository
Integration *postgres.IntegrationRepository
Expand Down Expand Up @@ -183,12 +199,28 @@ func NewRepositories(db *postgres.DB) *Repositories {
PentestTemplate: postgres.NewPentestTemplateRepository(db),
PentestReport: postgres.NewPentestReportRepository(db),

// Attachments
Attachment: postgres.NewAttachmentRepository(db),

// Compliance
ComplianceFramework: postgres.NewComplianceFrameworkRepository(db),
ComplianceControl: postgres.NewComplianceControlRepository(db),
ComplianceAssessment: postgres.NewComplianceAssessmentRepository(db),
ComplianceMapping: postgres.NewComplianceMappingRepository(db),

// Attack Simulation & Control Testing
Simulation: postgres.NewSimulationRepository(db),
ControlTest: postgres.NewControlTestRepository(db),

// Threat Actor Intelligence
ThreatActor: postgres.NewThreatActorRepository(db),

// Remediation Campaigns
RemediationCampaign: postgres.NewRemediationCampaignRepository(db),

// Business Units
BusinessUnit: postgres.NewBusinessUnitRepository(db),

SLA: postgres.NewSLAPolicyRepository(db),
Integration: postgres.NewIntegrationRepository(db),
// IntegrationSCMExt and IntegrationNotificationExt initialized after Integration
Expand Down
97 changes: 91 additions & 6 deletions cmd/server/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"github.com/openctemio/api/internal/infra/jobs"
"github.com/openctemio/api/internal/infra/llm"
"github.com/openctemio/api/internal/infra/redis"
"github.com/openctemio/api/internal/infra/storage"
"github.com/openctemio/api/internal/infra/websocket"
"github.com/openctemio/api/pkg/domain/attachment"
"github.com/openctemio/api/pkg/crypto"
"github.com/openctemio/api/pkg/domain/suppression"
"github.com/openctemio/api/pkg/email"
Expand Down Expand Up @@ -120,18 +122,39 @@ type Services struct {
PermVersion *app.PermissionVersionService
PermCache *app.PermissionCacheService

// Membership cache (Redis-backed wrapper around tenant.Repository
// .GetMembership). Read by RequireMembership +
// RequireActiveMembershipFromJWT middlewares so the membership
// status check on every tenant-scoped request becomes a Redis GET
// instead of a DB round trip. Invalidated by TenantService when
// role / status / membership rows change.
MembershipCache *app.MembershipCacheService

// Module Service (OSS - all modules enabled, UI metadata only)
Module *app.ModuleService

// SLA
SLA *app.SLAService

// Pentest
Pentest *app.PentestService
Pentest *app.PentestService
Attachment *app.AttachmentService

// Compliance
Compliance *app.ComplianceService

// Attack Simulation & Control Testing
Simulation *app.SimulationService

// Threat Actor Intelligence
ThreatActor *app.ThreatActorService

// Remediation Campaigns
RemediationCampaign *app.RemediationCampaignService

// Business Units
BusinessUnit *app.BusinessUnitService

// API Keys & Webhooks
APIKey *app.APIKeyService
Webhook *app.WebhookService
Expand Down Expand Up @@ -258,9 +281,52 @@ func NewServices(deps *ServiceDeps) (*Services, error) {
// Wire unified finding repository for CTEM integration (pentest findings → findings table)
s.Pentest.SetUnifiedFindingRepository(repos.Finding)
s.Pentest.SetCampaignMemberRepository(repos.PentestCampaignMember)
s.Pentest.SetAuditService(s.Audit) // audit logging for team changes + status changes
s.Pentest.SetFindingActivityService(s.FindingActivity) // finding activity trail
// Note: Pentest notification wiring happens later after NotificationService is initialized

// Initialize Attachment service (file upload/download).
// Storage provider selected via STORAGE_PROVIDER env var (default: "local").
// Local path configurable via STORAGE_LOCAL_PATH (default: ./data/attachments).
// In Docker: mount a volume at the local path to persist across rebuilds.
var fileStorage attachment.FileStorage
switch cfg.Storage.Provider {
case "local", "":
storagePath := cfg.Storage.LocalPath
if storagePath == "" {
storagePath = "./data/attachments"
}
fileStorage = storage.NewLocalStorage(storagePath)
log.Info("attachment storage: local filesystem", "path", storagePath)
default:
// Future: case "s3", "minio", "gcs" → initialize respective provider
log.Warn("unsupported storage provider, falling back to local", "provider", cfg.Storage.Provider)
fileStorage = storage.NewLocalStorage("./data/attachments")
}
s.Attachment = app.NewAttachmentService(repos.Attachment, fileStorage, log)
// Wire per-tenant storage resolution (tenants can configure S3/MinIO in settings)
storageResolver := app.NewSettingsStorageResolver(deps.DB, s.Encryptor, log)
s.Attachment.SetTenantStorageResolver(storageResolver, func(cfg attachment.StorageConfig) (attachment.FileStorage, error) {
switch cfg.Provider {
case "local":
basePath := cfg.BasePath
if basePath == "" {
basePath = "./data/attachments"
}
return storage.NewLocalStorage(basePath), nil
case "s3", "minio":
return storage.NewS3Storage(cfg.Bucket, cfg.Region, cfg.Endpoint, cfg.AccessKey, cfg.SecretKey)
default:
return nil, fmt.Errorf("unsupported tenant storage provider: %s", cfg.Provider)
}
})

// Initialize Compliance service
s.Simulation = app.NewSimulationService(repos.Simulation, repos.ControlTest, log)
s.ThreatActor = app.NewThreatActorService(repos.ThreatActor, log)
s.RemediationCampaign = app.NewRemediationCampaignService(repos.RemediationCampaign, log)
s.BusinessUnit = app.NewBusinessUnitService(repos.BusinessUnit, log)

s.Compliance = app.NewComplianceService(
repos.ComplianceFramework, repos.ComplianceControl,
repos.ComplianceAssessment, repos.ComplianceMapping, log,
Expand Down Expand Up @@ -508,6 +574,14 @@ func NewServices(deps *ServiceDeps) (*Services, error) {
return nil, fmt.Errorf("failed to initialize permission cache service: %w", err)
}

// Initialize membership cache. Hard error if Redis is unreachable
// at boot — without this cache the RequireMembership middleware
// hammers the database on every request.
s.MembershipCache, err = app.NewMembershipCacheService(deps.RedisClient, repos.Tenant, log)
if err != nil {
return nil, fmt.Errorf("failed to initialize membership cache service: %w", err)
}

s.Role = app.NewRoleService(repos.Role, repos.RolePermission, log,
app.WithRoleAuditService(s.Audit),
app.WithRolePermissionVersionService(s.PermVersion),
Expand All @@ -517,6 +591,10 @@ func NewServices(deps *ServiceDeps) (*Services, error) {
// Wire permission services to tenant service
s.Tenant.SetPermissionServices(s.PermCache, s.PermVersion)

// Wire membership cache so mutations (suspend / reactivate / role
// change / member removal) can drop the cached entry immediately.
s.Tenant.SetMembershipCache(s.MembershipCache)

// Initialize licensing service (OSS edition - modules from database)
s.Module = app.NewModuleService(repos.Module, log)
s.Module.SetTenantModuleRepo(repos.TenantModule)
Expand Down Expand Up @@ -592,6 +670,12 @@ func (s *Services) InitAuthServices(cfg *config.Config, repos *Repositories, log
// Wire session service to user service for session revocation on suspension
s.User.SetSessionService(s.Session)

// Wire session service to tenant service so SuspendMember can revoke
// all sessions of a suspended user immediately. Without this, the
// user's existing JWT continues to work on JWT-claim-scoped routes
// (e.g. /api/v1/me/*, /api/v1/notifications) until it expires.
s.Tenant.SetSessionService(s.Session)

// Initialize SSO service for per-tenant identity provider authentication
s.SSO = app.NewSSOService(
repos.IdentityProvider,
Expand Down Expand Up @@ -630,11 +714,12 @@ func (s *Services) InitEmailServices(cfg *config.Config, log *logger.Logger) err
return nil
}

// SetEmailEnqueuer sets the email job enqueuer.
func (s *Services) SetEmailEnqueuer(enqueuer app.EmailJobEnqueuer) {
s.EmailEnqueue = enqueuer
s.Tenant = app.NewTenantService(nil, nil, app.WithEmailEnqueuer(enqueuer))
}
// Note: SetEmailEnqueuer was removed. The previous implementation
// rebuilt s.Tenant with `app.NewTenantService(nil, nil, ...)`, which
// dropped the repo and the logger and would panic on any subsequent
// call. The actual wiring of the email enqueuer happens in main.go
// where it can also re-attach the permission and session services
// after the tenant service is reconstructed.

// initEncryptor initializes the credentials encryptor.
func initEncryptor(cfg *config.Config, log *logger.Logger) (crypto.Encryptor, error) {
Expand Down
Loading
Loading