Skip to content

Commit f1fe62f

Browse files
authored
Feat/poam phase1 (#338)
* feat(profile): add BuildByProps endpoint to profiles handler (local branch) * feat(profile): accept kebab-case keys in BuildByProps request * feat(profile): BuildByProps creates import and back-matter; validates rules * fix(profile): persist import include-controls and back-matter resources in BuildByProps * fix(profile): avoid duplicate imports/back-matter; persist associations after create and reload full profile * fix(profile): prevent duplicate include-controls groups by creating association explicitly * test(api): add integration test for BuildByProps to assert import and controls * lint(api): fix error string casing and remove debug print in integration test * ci(api): fix golangci-lint inputs; commit generated swagger docs to satisfy check-diff * lint(api): use strings.EqualFold for case-insensitive prop name/ns comparisons * docs(api): define BuildByPropsRequest/Response types and regenerate swagger to reflect correct payload schema * feat(poam): Phase 1 foundation – models, CRUD, swagger * docs(poam): add API design document * fix(poam): align Phase 1 implementation to Confluence authoritative design Corrects the initial Phase 1 implementation (BCH-1175) to match the Confluence v15 design document authored by Gustavo Carvalho. ## Model changes (poam_cf.go) - Rename deadline -> planned_completion_date (OSCAL-aligned) - Rename due_date -> scheduled_completion_date on milestones - Rename completed_at -> completion_date on milestones - Add lifecycle fields: source_type (enum: risk-promotion|manual|import), primary_owner_user_id, created_from_risk_id, acceptance_rationale, last_status_change_at, completed_at on PoamItem - Add order_index on CcfPoamItemMilestone - Add CcfPoamItemEvidenceLink, CcfPoamItemFindingLink, CcfPoamItemControlLink link tables (in addition to the existing risk link table) - Remove Jira-only fields: poc_name, poc_email, poc_phone, resource_required, remarks (these were ticket simplifications not in Confluence design) ## Migrator changes (migrator.go, tests/migrate.go) - Register all four link tables in both production and test migrators: CcfPoamItemRiskLink, CcfPoamItemEvidenceLink, CcfPoamItemFindingLink, CcfPoamItemControlLink ## Handler changes (poam_items.go) - Update createPoamRequest / updatePoamRequest to use Confluence field names - Add EvidenceIDs, FindingIDs, ControlRefs to create payload - Add overdueOnly and ownerRef query filters to GET /poam-items - Add link sub-resource endpoints: GET /:id/risks, /evidence, /controls, /findings - Set last_status_change_at on every status transition - Set completed_at on PoamItem when status -> completed - Rename controlRef -> poamControlRef to avoid collision with filter_import.go - Update milestone create/update to use scheduled_completion_date and completion_date field names ## Test changes (poam_items_integration_test.go) - Complete rewrite: 35 integration test cases covering - POST with minimal payload, milestones, risk links, all link types, invalid input - GET list with all filters: status, sspId, riskId, dueBefore, overdueOnly, ownerRef - GET /:id with milestone ordering and all link sets - PUT scalar fields, status->completed sets completed_at, status change sets last_status_change_at, not-found - DELETE with cascade verification across all link tables - GET/POST/PUT/DELETE milestones including completion_date auto-set - GET sub-resource link endpoints (risks, evidence, controls, findings) - Uniqueness constraint enforcement on duplicate risk links * fix(poam): remove duplicate last_status_change_at in Update handler The BeforeUpdate GORM hook on CcfPoamItem already calls tx.Statement.SetColumn('LastStatusChangeAt', ...) whenever the Status column changes. The Update handler was also setting updates['last_status_change_at'] in the same map, causing Postgres to receive a duplicate column assignment in the generated UPDATE statement (SQLSTATE 42601). Fix: remove the explicit map entry from the handler and rely exclusively on the BeforeUpdate hook. Also fixed integration test compile errors introduced by the merge with main: - RegisterHandlers signature changed from 6 to 5 args (removed extra nil) - controlRef renamed to poamControlRef to avoid package collision with filter_import.go * fix(poam): remove invalid GORM check: constraints causing migration failure in all test suites GORM's check: tag generates CHECK (tablename_columnname IN (...)) which is invalid Postgres syntax - Postgres expects just the column name. This caused every integration test suite's MigrateUp to fail with: ERROR: column "ccf_poam_items_status" does not exist (SQLSTATE 42703) Changes: - Remove check: tags from CcfPoamItem.Status, CcfPoamItem.SourceType, and CcfPoamItemMilestone.Status (no other model in the codebase uses check:) - Remove now-unused gorm.io/gorm import from poam_cf.go - Remove BeforeUpdate hook (map-based Updates() bypasses GORM hooks entirely; last_status_change_at is set directly in the handler's updates map instead) - Re-add last_status_change_at to the Update handler's updates map (was removed in previous commit to fix a duplicate-column error, but the BeforeUpdate hook cannot fire on map-based updates so it must be explicit) * refactor(poam): introduce DDD service layer; add all link CRUD endpoints - Extract all DB logic from handler into internal/service/relational/poam/ - models.go: PoamItem, PoamItemMilestone, four link types, status/source constants, BeforeCreate UUID hook, CreateParams, UpdateParams, CreateMilestoneParams, UpdateMilestoneParams, ListFilters, ControlRef - queries.go: ApplyFilters (status, sspId, riskId, deadlineBefore, overdueOnly, ownerRef) - service.go: PoamService with List, Create, GetByID, Update, Delete, EnsureExists, EnsureSSPExists, ListMilestones, AddMilestone, UpdateMilestone, DeleteMilestone, ListRiskLinks, AddRiskLink, DeleteRiskLink, ListEvidenceLinks, AddEvidenceLink, DeleteEvidenceLink, ListControlLinks, AddControlLink, DeleteControlLink, ListFindings, AddFindingLink, DeleteFindingLink - Rewrite handler (poam_items.go) to use service only — zero gorm imports; typed response structs (poamItemResponse, milestoneResponse, link types); @Security OAuth2Password on all Swagger annotations - Add all missing link CRUD endpoints: POST/DELETE /:id/risks/:riskId POST/DELETE /:id/evidence/:evidenceId POST/DELETE /:id/controls (catalogId+controlId path) POST/DELETE /:id/findings/:findingId - Fix check: GORM tag bug (generated invalid Postgres CHECK constraint with tablename_column prefix); remove BeforeUpdate hook (bypassed by map-based Updates()); set last_status_change_at directly in update map - Update migrator.go and tests/migrate.go to use new poam package types - Remove old poam_cf.go from relational package - Rewrite integration tests: use poamsvc types for seeding, correct response struct unmarshalling, idempotent duplicate-link test Sandbox validated: 35/35 tests pass against live Postgres instance * fix(poam): address all Copilot and Gus PR review feedback - Remove docs/POAM-Design.md (design belongs in Confluence, not repo) - Add has-many associations for all 4 link tables on PoamItem model - GetByID now preloads RiskLinks, EvidenceLinks, ControlLinks, FindingLinks - toPoamItemResponse populates all link arrays in the response body - Update handler uses typed struct Save() not raw map[string]interface{} - UpdatePoamItemParams includes AddRiskIDs/RemoveRiskIDs and equivalents for evidence, controls, findings (Gus: link management in Update) - validate:required tags on createPoamItemRequest.SspID and Title; ctx.Validate() called before processing (Copilot: missing validation) - parsePoamListFilters returns 400 on malformed UUID/RFC3339 params (Copilot: invalid query params silently ignored) - last_status_change_at stamped only on actual status transition (Copilot: was stamped even when status unchanged) - completedAt is server-controlled only; not settable via payload (Copilot: completedAt could be set via request body) - All First() errors discriminated: gorm.ErrRecordNotFound -> 404, other errors -> 500 (Copilot: all DB errors mapped to 404) - Link tables use composite primaryKey + constraint:OnDelete:CASCADE matching the Risk service pattern (Copilot item 13) - Integration tests updated to use poamsvc types and new response shapes - Sandbox validation: 35/35 tests pass against live Postgres instance * fix(poam): stage remaining review fixes (api.go wiring, queries.go, test updates, swagger regen) * fix(poam): correct stale type names in integration test Rename references to match the DDD-refactored handler types: - createPoamRequest -> createPoamItemRequest - updatePoamRequest -> updatePoamItemRequest - poamControlRef -> poamControlRefRequest - poamMilestoneResponse -> milestoneResponse (unexported, same package) All 35 integration test cases now compile and reference the correct types from the service-backed handler. * chore(poam): regenerate swagger docs and apply swag fmt Run 'make swag' (go tool swag init + swag fmt) to bring the committed docs/swagger.{json,yaml,docs.go} in sync with the current POAM handler annotations, and apply whitespace-only alignment fixes produced by swag fmt to poam_items.go, models.go, and service.go. This fixes the check-diff CI job which regenerates swagger and asserts a clean working tree. * fix(poam): rename poamAddControlLinkRequest -> poamControlRefRequest in test The AddControlLink test at line 910 still referenced the old type name poamAddControlLinkRequest. The correct type is poamControlRefRequest (the same struct used for both create-time and standalone link endpoints). go vet -tags integration now passes cleanly. * fix(poam): add JWT auth tokens to all integration test requests The POAM route group uses JWTMiddleware, so every test request must carry a valid Bearer token. Introduce an authedReq() suite helper that calls GetAuthToken() and sets the Authorization header, then update all 47 HTTP calls in the test file to use it. Also remove the now-unused 'strings' import and the blank-identifier workaround that was keeping it alive. * fix(poam): seed SSP record in TestCreate_* integration tests The Create handler calls EnsureSSPExists which queries system_security_plans. Tests that generate a random sspID without inserting the corresponding row were getting HTTP 404. Add an ensureSSP() helper and call it at the start of each TestCreate_* test that submits a valid sspId to the API. * fix(poam): correct dueBefore query param to deadlineBefore in test The List handler reads 'deadlineBefore' (not 'dueBefore') from the query string. The test was sending the wrong param name so the filter was never applied, causing all items to be returned instead of just the past-due one. * fix: address PR review comments - orderIndex in createMilestoneRequest changed to *int so explicit 0 is distinguishable from an omitted field; inline milestone loop in Create handler falls back to slice position when nil - overdueOnly filter now includes status='overdue' in the IN clause so items already persisted with that status are not silently excluded - removed blank identifier var _ = relational.SystemSecurityPlan{} and the relational import from models.go (import only needed in service.go) - added DB-level FK absence comments on all four link tables explaining the intentional cross-bounded-context design decision - added SSP-scoped routes /system-security-plans/:sspId/poam-items via RegisterSSPScoped; List and Create handlers inject the path param into filters/body automatically so clients don't repeat the SSP ID - regenerated Swagger docs * fix(api): restore SSP-scoped risk routes dropped during rebase conflict resolution * fix(oscal): use composite catalog+ID key in mergeControls/mergeGroups to prevent cross-catalog collisions - Adds controlMergeKey and groupMergeKey structs (matching main branch implementation) - Fixes TestProfileControlMerging/CrossCatalog and TestProfileGroupMerging unit tests - Also fixes profiles_integration_test.go to pass nil evidenceSvc to RegisterHandlers (signature changed in main to require *APIServices) --------- Co-authored-by: AKAbdulHanif <AKAbdulHanif@users.noreply.github.com>
1 parent 67f6327 commit f1fe62f

File tree

13 files changed

+8870
-1735
lines changed

13 files changed

+8870
-1735
lines changed

docs/docs.go

Lines changed: 2178 additions & 463 deletions
Large diffs are not rendered by default.

docs/swagger.json

Lines changed: 2178 additions & 463 deletions
Large diffs are not rendered by default.

docs/swagger.yaml

Lines changed: 1136 additions & 45 deletions
Large diffs are not rendered by default.

internal/api/handler/api.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/compliance-framework/api/internal/config"
1111
"github.com/compliance-framework/api/internal/service/digest"
1212
evidencesvc "github.com/compliance-framework/api/internal/service/relational/evidence"
13+
poamsvc "github.com/compliance-framework/api/internal/service/relational/poam"
1314
workflowsvc "github.com/compliance-framework/api/internal/service/relational/workflows"
1415
"github.com/compliance-framework/api/internal/workflow"
1516
"github.com/labstack/echo/v4"
@@ -48,15 +49,25 @@ func RegisterHandlers(server *api.Server, logger *zap.SugaredLogger, db *gorm.DB
4849
evidenceHandler := NewEvidenceHandler(logger, services.EvidenceService)
4950
evidenceHandler.Register(server.API().Group("/evidence"))
5051

52+
poamService := poamsvc.NewPoamService(db)
53+
poamHandler := NewPoamItemsHandler(poamService, logger)
54+
// Flat route: /api/poam-items (supports ?sspId= query filter)
55+
poamGroup := server.API().Group("/poam-items")
56+
poamGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
57+
poamHandler.Register(poamGroup)
58+
// SSP-scoped route: /api/system-security-plans/:sspId/poam-items
59+
// The :sspId path param is automatically injected into list/create filters.
60+
sspPoamGroup := server.API().Group("/system-security-plans/:sspId/poam-items")
61+
sspPoamGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
62+
poamHandler.RegisterSSPScoped(sspPoamGroup)
63+
5164
riskHandler := NewRiskHandler(logger, db)
5265
riskGroup := server.API().Group("/risks")
5366
riskGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
5467
riskHandler.Register(riskGroup)
55-
5668
sspRiskGroup := server.API().Group("/ssp/:sspId/risks")
5769
sspRiskGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
5870
riskHandler.RegisterSSPScoped(sspRiskGroup)
59-
6071
riskTemplateHandler := templatehandlers.NewRiskTemplateHandler(logger, db)
6172
riskTemplateGroup := server.API().Group("/risk-templates")
6273
riskTemplateGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))

0 commit comments

Comments
 (0)