Skip to content

fix: preserve catalog-canonical casing for control IDs#421

Merged
gusfcarvalho merged 2 commits into
mainfrom
claude/objective-chatelet-facab8
Jun 16, 2026
Merged

fix: preserve catalog-canonical casing for control IDs#421
gusfcarvalho merged 2 commits into
mainfrom
claude/objective-chatelet-facab8

Conversation

@gusfcarvalho

Copy link
Copy Markdown
Contributor

Problem

Implemented requirements auto-created when a profile is attached to an SSP were stored with lowercased control IDs (gd.sec.c08) while the catalog and profiles use canonical mixed casing (GD.Sec.C08). The lowercasing came from normalizeControlID (strings.ToLower) introduced in #380, applied to the stored value in the AddProfile/AttachProfile fan-out.

Because the frontend resolves control display by exact-string match against the catalog, the 12 lowercased requirements on the ToDo Demo SSP failed resolution and rendered as bare IDs. Other write paths (direct create, OSCAL import) already stored canonical casing — so the inconsistency was the real bug.

Fix

Converge storage on catalog-canonical casing, keep all matching case-insensitive (every downstream join already folds with UPPER(...)):

  • dedupeControlIDs — new helper: case-insensitive dedup that preserves first-seen casing. normalizeControlID/normalizeControlIDs are now documented as comparison-key / SQL LOWER(...) IN helpers only — never stored or displayed.
  • Resolvers (getControlIDsForProfile, getControlIDsForAllProfiles, extractControlIDsFromProfile) return canonical casing.
  • Auto-create loops (AddProfile, AttachProfile) persist the control ID verbatim; the lowercased value is used only as the existingMap dedup key.
  • Three LOWER(control_id) IN ? filter sites lower their arg explicitly now that the resolver returns canonical casing (this was the load-bearing dependency a naive fix would break).
  • Hardening — canonicalizeControlID: direct CreateImplementedRequirement/UpdateImplementedRequirement snap client-supplied casing to the bound profile's profile_controls form, falling back to the input verbatim when the control isn't part of a bound profile.

Tests

  • Added TestImplementedRequirements_PreserveCanonicalCasing — uses mixed-case profile controls and asserts (a) auto-created IRs keep canonical casing / aren't lowercased, and (b) a direct POST with gd.conf.c01 is stored as GD.Conf.C01. The pre-existing tests only used lowercase IDs, so they could never have caught this.
  • go build + go vet clean; all three SSP integration suites pass (profiles, SSP API, component suggestions); golangci-lint adds 0 new issues.

Notes

  • No backfill migration — the affected environment is ephemeral, so existing lowercase rows aren't worth migrating. In a persistent env, a re-case migration matching UPPER(ir.control_id) = UPPER(pc.control_id) would be needed for already-stored rows.

🤖 Generated with Claude Code

gusfcarvalho and others added 2 commits June 16, 2026 08:23
ImplementedRequirements auto-created when a profile is attached to an SSP
were stored with lowercased control IDs (gd.sec.c08) because the profile
auto-create path applied normalizeControlID (strings.ToLower) to the stored
value. The catalog and profiles use canonical mixed casing (GD.Sec.C08), so
the lowercased rows failed exact-string resolution downstream and rendered
as bare IDs.

Converge storage on catalog-canonical casing while keeping all matching
case-insensitive:

- Add dedupeControlIDs (case-insensitive dedup that preserves first-seen
  casing); reserve normalizeControlID/normalizeControlIDs for comparison
  keys and SQL LOWER(...) IN lists only, never stored or displayed values.
- Resolver funcs (getControlIDsForProfile, getControlIDsForAllProfiles,
  extractControlIDsFromProfile) now return canonical casing.
- Profile auto-create loops (AddProfile, AttachProfile) persist the control
  ID verbatim; the lowercased value is used only as the dedup key.
- The three LOWER(control_id) IN ? filter sites lower their arg explicitly
  now that the resolver returns canonical casing.
- Add canonicalizeControlID: direct Create/Update of implemented
  requirements snap client-supplied casing to the bound profile's
  profile_controls form, falling back to the input when not in a profile.
- Add TestImplementedRequirements_PreserveCanonicalCasing (the existing
  tests only exercised lowercase IDs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The head commit changed during the review from dc98562 to 7035ae7.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gusfcarvalho gusfcarvalho merged commit 9e833f9 into main Jun 16, 2026
5 checks passed
@gusfcarvalho gusfcarvalho deleted the claude/objective-chatelet-facab8 branch June 16, 2026 11:48
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.

1 participant