Skip to content

Conversation

@AKAbdulHanif
Copy link
Contributor

Adds POST /api/oscal/profiles/build-props to create profiles by matching catalog control props; persists a single import and back-matter resource; includes matched control IDs; reloads full profile for an accurate response; adds integration test.

Changes

  • Endpoint implementation: api/internal/api/handler/oscal/profiles.go:81
  • Validates catalogId (UUID) and rules with non-empty operator / value
  • Persists:
    • BackMatter resource linking to # (one per build)
      • profiles.go:208–225
    • Import pointing to the BackMatter resource (one per build)
      • profiles.go:226–231
    • IncludeControls group with matched IDs (single explicit association)
      • profiles.go:232–239
  • Syncs pivot and reloads full profile for the response
    • Sync: profiles.go:1220
    • Reload: profiles.go:245–251
  • Prevents duplicate groups/imports by creating associations exactly once

Strategies:

  • all : every rule must match at least one prop on a control
  • any : include controls if any rule matches
  • Operators: equals , contains , regex , in (comma-separated list)
  • Response includes profileId , controlIds , and full profile with a single import

Tests

  • Integration: api/internal/api/handler/oscal/profiles_integration_test.go:528
    • Seeds a minimal catalog with a “technical” control
    • Builds profile, asserts 201, one matched control, and exactly one import
  • Existing integration tests cover profile list/get/imports/back-matter/modify/merge

Verification

Acceptance Criteria

  • Build returns 201, non-empty controlIds , exactly one import

  • Profile Controls UI shows one imported catalog and included controls without duplicates

  • Risk/Impact

  • Low, localized to build flow; existing endpoints unchanged

  • Backward compatible

  • Rollout

  • Build/push API image and restart localdev

  • Validate a fresh build shows correct imports and controls

@AKAbdulHanif AKAbdulHanif self-assigned this Jan 22, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an API endpoint to build an OSCAL Profile by selecting catalog controls whose props match caller-supplied rules, and updates docs/tests accordingly.

Changes:

  • Adds POST /api/oscal/profiles/build-props handler that matches catalog controls by prop rules, persists back-matter + single import + include-controls, syncs pivots, and returns the full profile with matched control IDs.
  • Adds an integration test covering build-by-props behavior and validates exactly one persisted import.
  • Updates generated Swagger artifacts and tweaks CI golangci-lint action cache settings.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
internal/api/handler/oscal/profiles.go Implements and registers BuildByProps, plus prop-matching helper logic.
internal/api/handler/oscal/profiles_integration_test.go Adds integration coverage for build-by-props and removes debugging output/import.
docs/swagger.yaml Adds the new endpoint to OpenAPI YAML output.
docs/swagger.json Adds the new endpoint to OpenAPI JSON output.
docs/docs.go Updates embedded swagger template with the new endpoint.
.github/workflows/ci.yml Updates golangci-lint action cache-related inputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.GenericDataResponse-oscal_ProfileHandler"
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded swagger template also uses handler.GenericDataResponse-oscal_ProfileHandler for the 201 response, which won’t describe the actual response fields for BuildByProps. Regenerate swagger after defining a named response type so the template reflects the real payload schema.

Suggested change
"$ref": "#/definitions/handler.GenericDataResponse-oscal_ProfileHandler"
"$ref": "#/definitions/oscal.ProfileHandler"

Copilot uses AI. Check for mistakes.
name: request
required: true
schema:
$ref: '#/definitions/oscal.ProfileHandler'
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request body schema $ref is #/definitions/oscal.ProfileHandler, which is an empty object definition. This makes the OpenAPI for /oscal/profiles/build-props unusable for clients. After switching to named request/response structs in the handler, regenerate swagger so this points to the real BuildByProps request schema.

Suggested change
$ref: '#/definitions/oscal.ProfileHandler'
$ref: '#/definitions/oscal.BuildByPropsRequest'

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +153
if errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.JSON(http.StatusNotFound, api.NewError(err))
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Find(&controls) on a slice won’t return gorm.ErrRecordNotFound; the errors.Is(err, gorm.ErrRecordNotFound) branch is effectively dead code. If you want a 404 when the catalog doesn’t exist, check catalog existence explicitly (and return 404), otherwise remove the ErrRecordNotFound handling here.

Suggested change
if errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.JSON(http.StatusNotFound, api.NewError(err))
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complementing - we should check. if the catalog length is 0 and return a 404.

"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.GenericDataResponse-oscal_ProfileHandler"
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated swagger JSON 201 response schema currently points to handler.GenericDataResponse-oscal_ProfileHandler, which is based on an empty oscal.ProfileHandler schema rather than the real BuildByProps response body. Regenerate swagger after defining a named response type so clients see profileId, controlIds, and profile in the schema.

Suggested change
"$ref": "#/definitions/handler.GenericDataResponse-oscal_ProfileHandler"
"type": "object",
"properties": {
"profileId": {
"type": "string"
},
"controlIds": {
"type": "array",
"items": {
"type": "string"
}
},
"profile": {
"type": "object"
}
}

Copilot uses AI. Check for mistakes.
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/oscal.ProfileHandler"
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs.go embeds the generated OpenAPI template and currently references #/definitions/oscal.ProfileHandler as the request schema for /oscal/profiles/build-props, but that definition is empty. After adding named request/response structs in the handler, regenerate the swagger artifacts so this embedded template includes the correct schema refs.

Suggested change
"$ref": "#/definitions/oscal.ProfileHandler"
"type": "object"

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +282
m, _ := func() (bool, error) {
// simple regex match
re, err := regexp.Compile(r.Value)
if err != nil {
return false, err
}
return re.MatchString(p.Value), nil
}()
return m
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex operator compiles the pattern inside the matching loop and then ignores compilation errors (treating them as a non-match). Pre-compile regex patterns once during request validation and return 400 Bad Request if any pattern is invalid; this avoids extra work and prevents silent “zero matches” on invalid regex.

Suggested change
m, _ := func() (bool, error) {
// simple regex match
re, err := regexp.Compile(r.Value)
if err != nil {
return false, err
}
return re.MatchString(p.Value), nil
}()
return m
// simple regex match; handle compile errors explicitly
re, err := regexp.Compile(r.Value)
if err != nil {
return false
}
return re.MatchString(p.Value)

Copilot uses AI. Check for mistakes.
ID: "ac-1",
Title: "Access Control 1",
CatalogID: catID,
Props: []relational.Prop{{Name: "class", Value: "technical"}},
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relational.Control.Props is datatypes.JSONSlice[relational.Prop], so assigning a []relational.Prop slice literal here won’t compile. Initialize it using datatypes.NewJSONSlice(...) (and add the gorm.io/datatypes import) or use a properly typed datatypes.JSONSlice[relational.Prop]{...} literal.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if that was really the case the test suite would fail... and it doesn't.

Comment on lines +201 to +204
if err := h.db.Create(profile).Error; err != nil {
h.sugar.Errorw("failed to create profile from props", "error", err)
return ctx.JSON(http.StatusInternalServerError, api.NewError(err))
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint performs multiple dependent writes (profile, back-matter, resource, import, include-controls) without a transaction. If any step fails after the profile is created, the DB can be left with a partially-built profile. Wrap the whole build flow in db.Transaction(...) so failures roll back cleanly.

Copilot uses AI. Check for mistakes.
Comment on lines +13765 to +13768
"201":
description: Created
schema:
$ref: '#/definitions/handler.GenericDataResponse-oscal_ProfileHandler'
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 201 response schema $ref is #/definitions/handler.GenericDataResponse-oscal_ProfileHandler, which is derived from an empty oscal.ProfileHandler schema rather than the actual response payload (profileId/controlIds/profile). Define a named response type in code and regenerate swagger so the response schema matches reality.

Copilot uses AI. Check for mistakes.
Comment on lines +11663 to +11665
"schema": {
"$ref": "#/definitions/oscal.ProfileHandler"
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated swagger JSON for this endpoint references #/definitions/oscal.ProfileHandler as the request schema, but that definition is empty. After introducing named BuildByProps request/response types in the handler, regenerate swagger so this $ref points at the actual request schema.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
type rule struct {
Name string `json:"name"`
Ns string `json:"ns"`
Operator string `json:"operator"` // equals | contains | regex | in
Value string `json:"value"`
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably strongly type the string fields that are selections. Something like:

Suggested change
type rule struct {
Name string `json:"name"`
Ns string `json:"ns"`
Operator string `json:"operator"` // equals | contains | regex | in
Value string `json:"value"`
}
type rule struct {
Name string `json:"name"`
Ns string `json:"ns"`
Operator ruleOperation `json:"operator"` // equals | contains | regex | in
Value string `json:"value"`
}
type ruleOperation string
const (
operationEquals ruleOperation = "equals"
)

func (h *ProfileHandler) BuildByProps(ctx echo.Context) error {
type request struct {
CatalogID string `json:"catalogId"`
MatchStrategy string `json:"matchStrategy"` // all | any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here - we should probably strongly type this field.

}
var req request
var raw map[string]any
if err := json.NewDecoder(ctx.Request().Body).Decode(&raw); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is helper function on echo context to Bind the request body into a structure:

err := ctx.Bind(&req) // Will add requests correctly

Is there a reason we can't follow this?

It would align with the other handlers we have.

h.sugar.Errorw("failed to list catalog controls", "catalogId", req.CatalogID, "error", err)
return ctx.JSON(http.StatusInternalServerError, api.NewError(err))
}
matchAll := strings.ToLower(req.MatchStrategy) == "all"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whenever we upgrade the MatchStrategy to be strongly typed, this also becomes a nicer, defined check, with no need to strings.ToLower before that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants