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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import gov.nist.secauto.metaschema.model.common.metapath.item.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.model.common.util.ObjectUtils;
import gov.nist.secauto.oscal.lib.model.BackMatter.Resource;
import gov.nist.secauto.oscal.lib.model.Catalog;
import gov.nist.secauto.oscal.lib.model.CatalogGroup;
import gov.nist.secauto.oscal.lib.model.Control;
import gov.nist.secauto.oscal.lib.model.ControlPart;
Expand Down Expand Up @@ -237,6 +238,11 @@ public DefaultResult visitControl(IRequiredValueModelNodeItem item, DefaultResul
if (parent.getValue() instanceof Control && SelectionStatus.SELECTED.equals(index.getSelectionStatus(parent))) {
retval.removeControl(control);
}
// Cancel promotion of this control if control is already at the top level (control's parent is Catalog)
// If already at top level, then promotion is not needed because it was added by Import class
if (parent.getValue() instanceof Catalog) {
retval.removeControl(control);
}
} else {
// remove this control and promote any needed children

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,11 @@ public DefaultResult visitControl(
control.setId(entity.getIdentifier());

if (!SelectionStatus.SELECTED.equals(index.getSelectionStatus(parent))) {
// promote this control
retval.promoteControl(control);
// promote this control if control is not already at the top level (control's parent is Catalog)
// If already at top level, then promotion is not needed because it was added by Import class
if (!(parent.getValue() instanceof Catalog)) {
retval.promoteControl(control);
}
}
childResult.applyTo(control);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import gov.nist.secauto.metaschema.model.common.metapath.StaticContext;
import gov.nist.secauto.oscal.lib.OscalBindingContext;
import gov.nist.secauto.oscal.lib.model.Catalog;
import gov.nist.secauto.oscal.lib.model.Profile;
import gov.nist.secauto.oscal.lib.profile.resolver.selection.ImportCycleException;

import net.sf.saxon.s9api.Processor;
Expand Down Expand Up @@ -66,6 +67,8 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZoneOffset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
Expand Down Expand Up @@ -256,4 +259,49 @@ void testArsModerateProfile() throws IOException, ProfileResolutionException, UR

assertNotNull(resolvedCatalog);
}

// Test for https://github.com/usnistgov/liboscal-java/issues/360
// The issue:
// 1. Profile imports a catalog using <include-all/>
// 2. The catalog contains only controls at the top level, no groups.
// If both criteria are met, controls were added multiple times to the resolved
// profile because of the promoting controls process, which checks if controls
// need to be added again.
@Test
void testProfileResolverIssue360() throws IOException, ProfileResolutionException, URISyntaxException {
// Import profile
Path profilePath = Paths.get(JUNIT_TEST_PATH, "content/issue360-profile.xml");
assert profilePath != null;

// Resolve the profile
Catalog resolvedCatalog = resolveProfile(profilePath);
assertNotNull(resolvedCatalog);

// Write resolved profile to XML string
ISerializer<Catalog> serializer = OscalBindingContext.instance().newSerializer(Format.XML, Catalog.class);
String profileXMLString;
try (StringWriter writer = new StringWriter()) {
serializer.serialize(resolvedCatalog, writer);
profileXMLString = writer.toString();
}

// In XML string, count occurrences of control a1
Pattern controlAPattern = Pattern.compile("<control[^>]*id=\"a1\"[^>]*>");
Matcher matcherA = controlAPattern.matcher(profileXMLString);
int countA = 0;
while (matcherA.find()) {
countA++;
}

// In XML string, count occurrences of control b1
Pattern controlBPattern = Pattern.compile("<control[^>]*id=\"b1\"[^>]*>");
Matcher matcherB = controlBPattern.matcher(profileXMLString);
int countB = 0;
while (matcherB.find()) {
countB++;
}

assertEquals(1, countA, "Control a1 should appear only once in resolved profile");
assertEquals(1, countB, "Control b1 should appear only once in resolved profile");
}
}
28 changes: 28 additions & 0 deletions src/test/resources/content/issue360-catalog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<catalog xmlns="http://csrc.nist.gov/ns/oscal/1.0" uuid="9a6f55fc-7018-4eee-a8d4-a5796ed9d549">
<metadata>
<title>Issue 360 Catalog</title>
<last-modified>2026-08-17T18:19:00.817580161Z</last-modified>
<version>1.0.0</version>
<oscal-version>1.2.2</oscal-version>
</metadata>
<control id="a1">
<title>Control A1</title>
<param id="a1_prm1">
<label>A1 Parameter 1</label>
</param>
<prop name="label" value="first"/>
<part id="a1-stmt" name="statement">
<p>
A1 aaaaa <insert type="param" id-ref="a1_prm1"/>aaaaaaaaaa
</p>
</part>
</control>
<control id="b1">
<title>Control B1</title>
<prop name="label" value="second"/>
<part id="b1-stmt" name="statement">
<p>B1 bbbb bbbbbbb.</p>
</part>
</control>
</catalog>
15 changes: 15 additions & 0 deletions src/test/resources/content/issue360-profile.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<profile xmlns="http://csrc.nist.gov/ns/oscal/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://csrc.nist.gov/ns/oscal/1.0 ../../../oscal/xml/schema/oscal_complete_schema.xsd"
uuid="df19324b-aa44-4e22-a4d2-397bdc6bbaab">
<metadata>
<title>Issue 360 Profile</title>
<last-modified>2026-08-17T16:15:16-04:00</last-modified>
<version>1.0.0</version>
<oscal-version>1.2.2</oscal-version>
</metadata>
<import href="issue360-catalog.xml">
<include-all/>
</import>
</profile>
Loading