fix: preserve catalog-canonical casing for control IDs#421
Merged
Conversation
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>
|
Caution Review failedAn 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. Comment |
|
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. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fromnormalizeControlID(strings.ToLower) introduced in #380, applied to the stored value in theAddProfile/AttachProfilefan-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/normalizeControlIDsare now documented as comparison-key / SQLLOWER(...) INhelpers only — never stored or displayed.getControlIDsForProfile,getControlIDsForAllProfiles,extractControlIDsFromProfile) return canonical casing.AddProfile,AttachProfile) persist the control ID verbatim; the lowercased value is used only as theexistingMapdedup key.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).canonicalizeControlID: directCreateImplementedRequirement/UpdateImplementedRequirementsnap client-supplied casing to the bound profile'sprofile_controlsform, falling back to the input verbatim when the control isn't part of a bound profile.Tests
TestImplementedRequirements_PreserveCanonicalCasing— uses mixed-case profile controls and asserts (a) auto-created IRs keep canonical casing / aren't lowercased, and (b) a direct POST withgd.conf.c01is stored asGD.Conf.C01. The pre-existing tests only used lowercase IDs, so they could never have caught this.go build+go vetclean; all three SSP integration suites pass (profiles, SSP API, component suggestions);golangci-lintadds 0 new issues.Notes
UPPER(ir.control_id) = UPPER(pc.control_id)would be needed for already-stored rows.🤖 Generated with Claude Code