Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions src/main/metaschema-constraints/oscal-external-constraints.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,13 @@

<constraints>
<let var="resolved-profile-import" expression=".[@href] ! resolve-profile(doc(resolve-uri(Q{http://csrc.nist.gov/ns/oscal/1.0}resolve-reference(@href))))/catalog"/>
<index id="oscal-profile-import-index-control-id" name="profile-import-index-control-id" target="$resolved-profile-import//control">
<formal-name>In-Scope Control Identifiers</formal-name>
<description>An index of control identifiers that are in-scope for selection in the profile import.</description>
<key-field target="@id"/>
</index>
<index-has-key id="oscal-profile-import-has-key-include-exclude-control-id" name="profile-import-index-control-id" target="(include-controls|exclude-controls)/with-id">
<key-field target="."/>
</index-has-key>
<expect id="oscal-profile-import-include-exclude-control-id-in-imported-catalog"
target="(include-controls|exclude-controls)/with-id"
test="let $id := . return exists($resolved-profile-import//control[@id = $id])">
<formal-name>In-Scope Control Identifier</formal-name>
<description>Each referenced control identifier must match a control in the catalog resolved by the surrounding profile import.</description>
<message>Control identifier '{ . }' was not found in the imported catalog.</message>
</expect>
</constraints>
</context>
<context>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ 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.
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(profilePath).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;
Expand Down
23 changes: 23 additions & 0 deletions src/test/resources/content/issue-250/test-catalog-ac-1.json
Original file line number Diff line number Diff line change
@@ -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."
}
]
}
]
}
}
23 changes: 23 additions & 0 deletions src/test/resources/content/issue-250/test-catalog.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
}
36 changes: 36 additions & 0 deletions src/test/resources/content/issue-250/test-profile-reversed.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
36 changes: 36 additions & 0 deletions src/test/resources/content/issue-250/test-profile.json
Original file line number Diff line number Diff line change
@@ -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": "test-catalog-ac-1.json",
"include-controls": [
{
"with-ids": [
"ac-1"
]
}
]
},
{
"href": "test-catalog.json",
"include-controls": [
{
"with-ids": [
"test-01"
]
}
]
}
],
"merge": {
"as-is": true
}
}
}
Loading