Skip to content

Commit f3dce82

Browse files
fix: minor issues related to risk flow (#346)
* fix: minor issues related to risk flow Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> * fix: build fixes Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> * Update internal/api/handler/templates/evidence_template_integration_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: docs Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> --------- Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f1fe62f commit f3dce82

18 files changed

+7087
-4169
lines changed

docs/docs.go

Lines changed: 2132 additions & 1266 deletions
Large diffs are not rendered by default.

docs/swagger.json

Lines changed: 2132 additions & 1266 deletions
Large diffs are not rendered by default.

docs/swagger.yaml

Lines changed: 1577 additions & 1010 deletions
Large diffs are not rendered by default.

internal/api/handler/api.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,26 @@ func RegisterHandlers(server *api.Server, logger *zap.SugaredLogger, db *gorm.DB
6565
riskGroup := server.API().Group("/risks")
6666
riskGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
6767
riskHandler.Register(riskGroup)
68-
sspRiskGroup := server.API().Group("/ssp/:sspId/risks")
68+
69+
sspRiskGroup := server.API().Group("/oscal/system-security-plans/:sspId/risks")
6970
sspRiskGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
7071
riskHandler.RegisterSSPScoped(sspRiskGroup)
7172
riskTemplateHandler := templatehandlers.NewRiskTemplateHandler(logger, db)
72-
riskTemplateGroup := server.API().Group("/risk-templates")
73+
riskTemplateGroup := server.API().Group("/admin/risk-templates")
7374
riskTemplateGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
75+
riskTemplateGroup.Use(middleware.RequireAdminGroups(db, config, logger))
7476
riskTemplateHandler.Register(riskTemplateGroup)
7577

7678
subjectTemplateHandler := templatehandlers.NewSubjectTemplateHandler(logger, db)
77-
subjectTemplateGroup := server.API().Group("/subject-templates")
79+
subjectTemplateGroup := server.API().Group("/admin/subject-templates")
7880
subjectTemplateGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
81+
subjectTemplateGroup.Use(middleware.RequireAdminGroups(db, config, logger))
7982
subjectTemplateHandler.Register(subjectTemplateGroup)
8083

8184
evidenceTemplateHandler := templatehandlers.NewEvidenceTemplateHandler(logger, db)
82-
evidenceTemplateGroup := server.API().Group("/evidence-templates")
85+
evidenceTemplateGroup := server.API().Group("/admin/evidence-templates")
8386
evidenceTemplateGroup.Use(middleware.JWTMiddleware(config.JWTPublicKey))
87+
evidenceTemplateGroup.Use(middleware.RequireAdminGroups(db, config, logger))
8488
evidenceTemplateHandler.Register(evidenceTemplateGroup)
8589

8690
userHandler := NewUserHandler(logger, db)

internal/api/handler/evidence.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func (h *EvidenceHandler) Register(api *echo.Group) {
3434
api.POST("", h.Create)
3535
api.GET("/:id", h.Get)
3636
api.GET("/history/:id", h.History)
37+
api.GET("/latest/:id", h.Latest)
3738
api.POST("/search", h.Search)
3839
api.GET("/for-control/:id", h.ForControl)
3940
api.GET("/status-over-time/:id", h.StatusOverTimeByUUID)
@@ -473,7 +474,7 @@ func (h *EvidenceHandler) Get(ctx echo.Context) error {
473474
// @Description Retrieves a the history for a Evidence record by its UUID, including associated activities, inventory items, components, subjects, and labels.
474475
// @Tags Evidence
475476
// @Produce json
476-
// @Param id path string true "Evidence ID"
477+
// @Param id path string true "Evidence UUID"
477478
// @Success 200 {object} GenericDataListResponse[OscalLikeEvidence]
478479
// @Failure 400 {object} api.Error
479480
// @Failure 404 {object} api.Error
@@ -508,6 +509,43 @@ func (h *EvidenceHandler) History(ctx echo.Context) error {
508509
return ctx.JSON(http.StatusOK, GenericDataListResponse[*OscalLikeEvidence]{Data: output})
509510
}
510511

512+
// Latest godoc
513+
//
514+
// @Summary Get latest Evidence by UUID
515+
// @Description Retrieves the most recent Evidence record for a given UUID stream, including associated activities, inventory items, components, subjects, and labels.
516+
// @Tags Evidence
517+
// @Produce json
518+
// @Param id path string true "Evidence UUID"
519+
// @Success 200 {object} GenericDataResponse[OscalLikeEvidence]
520+
// @Failure 400 {object} api.Error
521+
// @Failure 404 {object} api.Error
522+
// @Failure 500 {object} api.Error
523+
// @Router /evidence/latest/{id} [get]
524+
func (h *EvidenceHandler) Latest(ctx echo.Context) error {
525+
idParam := ctx.Param("id")
526+
id, err := uuid.Parse(idParam)
527+
if err != nil {
528+
h.sugar.Warnw("Invalid evidence uuid", "id", idParam, "error", err)
529+
return ctx.JSON(http.StatusBadRequest, api.NewError(err))
530+
}
531+
532+
evidence, err := h.evidenceService.GetLatestByUUID(id)
533+
if err != nil {
534+
if errors.Is(err, gorm.ErrRecordNotFound) {
535+
return ctx.JSON(http.StatusNotFound, api.NewError(err))
536+
}
537+
h.sugar.Warnw("Failed to load latest evidence", "uuid", idParam, "error", err)
538+
return ctx.JSON(http.StatusInternalServerError, api.NewError(err))
539+
}
540+
541+
output := &OscalLikeEvidence{}
542+
if err = output.FromEvidence(evidence); err != nil {
543+
return ctx.JSON(http.StatusInternalServerError, api.NewError(err))
544+
}
545+
546+
return ctx.JSON(http.StatusOK, GenericDataResponse[*OscalLikeEvidence]{Data: output})
547+
}
548+
511549
// ForControl godoc
512550
//
513551
// @Summary List Evidence for a Control

internal/api/handler/oscal/profiles.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ type rule struct {
3333
Value string `json:"value"`
3434
}
3535

36+
type resolvedWithCatalogsResponse struct {
37+
ControlID string `json:"control-id"`
38+
CatalogID uuid.UUID `json:"catalog-id"`
39+
Title string `json:"title"`
40+
Class string `json:"class"`
41+
}
42+
3643
// BuildByPropsRequest represents the payload to build a Profile by matching control props.
3744
type BuildByPropsRequest struct {
3845
CatalogID string `json:"catalogId"`
@@ -62,6 +69,8 @@ func (h *ProfileHandler) Register(api *echo.Group) {
6269
api.POST("/build-props", h.BuildByProps)
6370
api.GET("/:id", h.Get)
6471
api.GET("/:id/resolved", h.Resolved)
72+
api.GET("/:id/resolved-with-catalogs", h.ResolvedWithCatalogs)
73+
api.GET("/:id/compliance-progress", h.ComplianceProgress)
6574

6675
api.GET("/:id/modify", h.GetModify)
6776
api.GET("/:id/back-matter", h.GetBackmatter)
@@ -448,6 +457,67 @@ func (h *ProfileHandler) Resolved(ctx echo.Context) error {
448457
return ctx.JSON(http.StatusOK, handler.GenericDataResponse[oscalTypes_1_1_3.Catalog]{Data: *catalog.MarshalOscal()})
449458
}
450459

460+
// ResolvedWithCatalogs godoc
461+
//
462+
// @Summary Get Resolved Profile with Catalog IDs
463+
// @Description Returns a simplified flat list of controls from a resolved profile with control-id, catalog-id, title, and class.
464+
// @Tags Profile
465+
// @Param id path string true "Profile ID"
466+
// @Produce json
467+
// @Success 200 {object} handler.GenericDataListResponse[resolvedWithCatalogsResponse]
468+
// @Failure 400 {object} api.Error
469+
// @Failure 401 {object} api.Error
470+
// @Failure 404 {object} api.Error
471+
// @Failure 500 {object} api.Error
472+
// @Security OAuth2Password
473+
// @Router /oscal/profiles/{id}/resolved-with-catalogs [get]
474+
func (h *ProfileHandler) ResolvedWithCatalogs(ctx echo.Context) error {
475+
idParam := ctx.Param("id")
476+
id, err := uuid.Parse(idParam)
477+
if err != nil {
478+
h.sugar.Errorw("error parsing UUID", "id", idParam, "error", err)
479+
return ctx.JSON(http.StatusBadRequest, api.NewError(err))
480+
}
481+
482+
profile, err := FindFullProfile(h.db, id)
483+
if err != nil {
484+
if errors.Is(err, gorm.ErrRecordNotFound) {
485+
return ctx.JSON(http.StatusNotFound, api.NewError(err))
486+
}
487+
h.sugar.Errorw("error finding profile", "id", idParam, "error", err)
488+
return ctx.JSON(http.StatusInternalServerError, api.NewError(err))
489+
}
490+
491+
catalog, err := GetControlCatalogFromBuiltProfile(profile, h.db)
492+
if err != nil {
493+
h.sugar.Errorw("error building control catalog", "id", id, "error", err)
494+
return ctx.JSON(http.StatusInternalServerError, api.NewError(err))
495+
}
496+
497+
// Flatten all controls from catalog and groups into a single list
498+
allControls := flattenControls(catalog.Controls)
499+
for _, group := range catalog.Groups {
500+
allControls = append(allControls, flattenControlsFromGroup(group)...)
501+
}
502+
503+
// Convert to simplified response
504+
response := make([]resolvedWithCatalogsResponse, len(allControls))
505+
for i, ctrl := range allControls {
506+
class := ""
507+
if ctrl.Class != nil {
508+
class = *ctrl.Class
509+
}
510+
response[i] = resolvedWithCatalogsResponse{
511+
ControlID: ctrl.ID,
512+
CatalogID: ctrl.CatalogID,
513+
Title: ctrl.Title,
514+
Class: class,
515+
}
516+
}
517+
518+
return ctx.JSON(http.StatusOK, handler.GenericDataListResponse[resolvedWithCatalogsResponse]{Data: response})
519+
}
520+
451521
// ListImports godoc
452522
//
453523
// @Summary List Imports
@@ -1648,7 +1718,29 @@ func FindOscalCatalogFromBackMatter(profile *relational.Profile, ref string) (uu
16481718
return uuid.Nil, errors.New("no valid catalog uuid was found within the backmatter. ref: " + ref)
16491719
}
16501720

1651-
// GatherControlIds extracts unique control IDs from an Import’s IncludeControls, avoiding duplicates.
1721+
// flattenControls recursively flattens a list of controls and their sub-controls
1722+
func flattenControls(controls []relational.Control) []relational.Control {
1723+
var result []relational.Control
1724+
for _, ctrl := range controls {
1725+
result = append(result, ctrl)
1726+
if len(ctrl.Controls) > 0 {
1727+
result = append(result, flattenControls(ctrl.Controls)...)
1728+
}
1729+
}
1730+
return result
1731+
}
1732+
1733+
// flattenControlsFromGroup recursively flattens controls from a group and its sub-groups
1734+
func flattenControlsFromGroup(group relational.Group) []relational.Control {
1735+
var result []relational.Control
1736+
result = append(result, flattenControls(group.Controls)...)
1737+
for _, subGroup := range group.Groups {
1738+
result = append(result, flattenControlsFromGroup(subGroup)...)
1739+
}
1740+
return result
1741+
}
1742+
1743+
// GatherControlIds extracts unique control IDs from an Import's IncludeControls, avoiding duplicates.
16521744
func GatherControlIds(imports relational.Import) []string {
16531745
var controlIds []string
16541746
seen := map[string]bool{}

0 commit comments

Comments
 (0)