From 93fde5997c4892f0ab663d1b297ca9f2aca66be6 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Thu, 23 Apr 2026 02:03:51 -0400 Subject: [PATCH 1/2] fix: validate profile imports against their own catalogs (oscal-cli#250) The oscal-profile-import-has-key-include-exclude-control-id constraint used a document-global named index (profile-import-index-control-id) built from each profile import's resolved catalog. With more than one import the index name collides, so only the first import's index is consulted when checking with-id entries on later imports, causing valid references to be reported as missing. Replace the index/index-has-key pair with an inline expect that uses the per-context $resolved-profile-import let binding to look up the referenced control id directly in the catalog resolved for the same import. Adds a regression test that validates a profile importing two separate catalogs (NIST 800-53 rev5 from the downloaded test content plus a local test catalog); previously failed on the second import, now passes. --- .../oscal-external-constraints.xml | 15 ++++---- .../lib/validation/OscalValidationTest.java | 21 +++++++++++ .../content/issue-250/test-catalog.json | 23 ++++++++++++ .../content/issue-250/test-profile.json | 36 +++++++++++++++++++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/content/issue-250/test-catalog.json create mode 100644 src/test/resources/content/issue-250/test-profile.json diff --git a/src/main/metaschema-constraints/oscal-external-constraints.xml b/src/main/metaschema-constraints/oscal-external-constraints.xml index 650a686e..0e9ee957 100644 --- a/src/main/metaschema-constraints/oscal-external-constraints.xml +++ b/src/main/metaschema-constraints/oscal-external-constraints.xml @@ -118,14 +118,13 @@ - - In-Scope Control Identifiers - An index of control identifiers that are in-scope for selection in the profile import. - - - - - + + In-Scope Control Identifier + Each referenced control identifier must match a control in the catalog resolved by the surrounding profile import. + Control identifier '{ . }' was not found in the imported catalog. + diff --git a/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java b/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java index ababfb1c..5253569a 100644 --- a/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java +++ b/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java @@ -112,6 +112,27 @@ void testValidateProfileWithMissingControls() assertFalse(validationResult.isPassing()); } + @Test + void testValidateProfileWithMultipleImports() + throws MetaschemaException, IOException, URISyntaxException, ConstraintValidationException { + // Regression test for oscal-cli#250: a profile that imports two catalogs + // should validate every with-id against the catalog referenced by the + // same import, not against an index built from the first import only. + IBindingContext bindingContext = OscalBindingContext.newInstance(); + + IValidationResult validationResult = bindingContext.validateWithConstraints( + Paths.get("src/test/resources/content/issue-250/test-profile.json").toUri(), + null); + + if (validationResult.isPassing()) { + LOGGER.info("The resource is valid."); + } else { + LOGGER.info("Validation identified the following issues:"); + new LoggingValidationHandler().handleResults(validationResult); + } + assertTrue(validationResult.isPassing()); + } + private static final class ValidationProvider implements ISchemaValidationProvider { @NonNull private final IModule module; diff --git a/src/test/resources/content/issue-250/test-catalog.json b/src/test/resources/content/issue-250/test-catalog.json new file mode 100644 index 00000000..fdfb6ba0 --- /dev/null +++ b/src/test/resources/content/issue-250/test-catalog.json @@ -0,0 +1,23 @@ +{ + "catalog": { + "uuid": "a7167eec-1ab2-4ec3-9921-5680981be857", + "metadata": { + "title": "Test Catalog", + "last-modified": "2026-02-25T02:49:04Z", + "version": "1.0.0", + "oscal-version": "1.2.1" + }, + "controls": [ + { + "id": "test-01", + "title": "TEST CONTROL", + "parts": [ + { + "name": "statement", + "prose": "Test content" + } + ] + } + ] + } +} diff --git a/src/test/resources/content/issue-250/test-profile.json b/src/test/resources/content/issue-250/test-profile.json new file mode 100644 index 00000000..0d1e81cd --- /dev/null +++ b/src/test/resources/content/issue-250/test-profile.json @@ -0,0 +1,36 @@ +{ + "profile": { + "uuid": "e285521b-890c-4b8e-b0c3-e72fac21a51e", + "metadata": { + "title": "Test Profile", + "last-modified": "2026-03-23T00:00:00Z", + "version": "1.0.0", + "oscal-version": "1.2.1" + }, + "imports": [ + { + "href": "../../../../../target/download/content/NIST_SP-800-53_rev5_catalog.json", + "include-controls": [ + { + "with-ids": [ + "ac-1" + ] + } + ] + }, + { + "href": "test-catalog.json", + "include-controls": [ + { + "with-ids": [ + "test-01" + ] + } + ] + } + ], + "merge": { + "as-is": true + } + } +} From 37d0871b6cf4dd1e0dbfda51b58060c04d0cb4a3 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Thu, 23 Apr 2026 02:16:15 -0400 Subject: [PATCH 2/2] test: hermetic fixture + reversed-import regression for oscal-cli#250 Replaces the build-download NIST catalog dependency with a minimal local AC-1 fixture so the test is independent of the download-maven plugin running during test-compile and works regardless of build phase ordering. Adds testValidateProfileWithMultipleImportsReversed to exercise the exact pre-fix asymmetry described in the original bug report (errors followed whichever catalog was imported second), extracting a shared assertMultiImportProfileValidates helper. --- .../lib/validation/OscalValidationTest.java | 17 ++++++++- .../content/issue-250/test-catalog-ac-1.json | 23 ++++++++++++ .../issue-250/test-profile-reversed.json | 36 +++++++++++++++++++ .../content/issue-250/test-profile.json | 2 +- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/content/issue-250/test-catalog-ac-1.json create mode 100644 src/test/resources/content/issue-250/test-profile-reversed.json diff --git a/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java b/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java index 5253569a..197d5d2c 100644 --- a/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java +++ b/src/test/java/dev/metaschema/oscal/lib/validation/OscalValidationTest.java @@ -118,10 +118,25 @@ void testValidateProfileWithMultipleImports() // Regression test for oscal-cli#250: a profile that imports two catalogs // should validate every with-id against the catalog referenced by the // same import, not against an index built from the first import only. + assertMultiImportProfileValidates("src/test/resources/content/issue-250/test-profile.json"); + } + + @Test + void testValidateProfileWithMultipleImportsReversed() + throws MetaschemaException, IOException, URISyntaxException, ConstraintValidationException { + // Companion regression for oscal-cli#250: the original bug report noted + // that reversing the import order moved the errors. This test validates + // the same two catalogs with their import order swapped to guard against + // any future reintroduction of import-order sensitivity. + assertMultiImportProfileValidates("src/test/resources/content/issue-250/test-profile-reversed.json"); + } + + private void assertMultiImportProfileValidates(@NonNull String profilePath) + throws MetaschemaException, IOException, ConstraintValidationException { IBindingContext bindingContext = OscalBindingContext.newInstance(); IValidationResult validationResult = bindingContext.validateWithConstraints( - Paths.get("src/test/resources/content/issue-250/test-profile.json").toUri(), + Paths.get(profilePath).toUri(), null); if (validationResult.isPassing()) { diff --git a/src/test/resources/content/issue-250/test-catalog-ac-1.json b/src/test/resources/content/issue-250/test-catalog-ac-1.json new file mode 100644 index 00000000..a7434c29 --- /dev/null +++ b/src/test/resources/content/issue-250/test-catalog-ac-1.json @@ -0,0 +1,23 @@ +{ + "catalog": { + "uuid": "5f6606a2-5d1f-4df2-8d5e-4c0e7b57bea6", + "metadata": { + "title": "Minimal AC Family Catalog", + "last-modified": "2026-02-25T02:49:04Z", + "version": "1.0.0", + "oscal-version": "1.2.1" + }, + "controls": [ + { + "id": "ac-1", + "title": "Access Control Policy and Procedures", + "parts": [ + { + "name": "statement", + "prose": "Minimal AC-1 statement used only to validate profile imports." + } + ] + } + ] + } +} diff --git a/src/test/resources/content/issue-250/test-profile-reversed.json b/src/test/resources/content/issue-250/test-profile-reversed.json new file mode 100644 index 00000000..48643802 --- /dev/null +++ b/src/test/resources/content/issue-250/test-profile-reversed.json @@ -0,0 +1,36 @@ +{ + "profile": { + "uuid": "13a94063-d30b-4fc8-bf3b-b75bf9da0b72", + "metadata": { + "title": "Test Profile (Reversed Imports)", + "last-modified": "2026-03-23T00:00:00Z", + "version": "1.0.0", + "oscal-version": "1.2.1" + }, + "imports": [ + { + "href": "test-catalog.json", + "include-controls": [ + { + "with-ids": [ + "test-01" + ] + } + ] + }, + { + "href": "test-catalog-ac-1.json", + "include-controls": [ + { + "with-ids": [ + "ac-1" + ] + } + ] + } + ], + "merge": { + "as-is": true + } + } +} diff --git a/src/test/resources/content/issue-250/test-profile.json b/src/test/resources/content/issue-250/test-profile.json index 0d1e81cd..7b3fe445 100644 --- a/src/test/resources/content/issue-250/test-profile.json +++ b/src/test/resources/content/issue-250/test-profile.json @@ -9,7 +9,7 @@ }, "imports": [ { - "href": "../../../../../target/download/content/NIST_SP-800-53_rev5_catalog.json", + "href": "test-catalog-ac-1.json", "include-controls": [ { "with-ids": [