Fork me on GitHub

Resolving Profiles

This guide explains how to resolve OSCAL profiles programmatically using liboscal-java.

OSCAL profiles are a powerful mechanism for customizing security control catalogs. Instead of copying and manually editing a catalog like NIST SP 800-53, organizations create profiles that:

  • Select controls - Choose which controls apply to their environment
  • Set parameters - Fill in organization-specific values for control parameters
  • Modify content - Add guidance, alter statements, or customize controls
  • Layer customizations - Import other profiles to build on existing baselines

However, many tools that consume OSCAL content expect a simple catalog, not a profile with references to external catalogs. Profile resolution is the process of “flattening” a profile into a self-contained catalog that includes all selected controls with all modifications applied.

For example, if you have a profile that imports NIST SP 800-53 and selects the FedRAMP High baseline controls, profile resolution produces a catalog containing just those controls, with any organization-specific modifications already applied.

Profile resolution takes an OSCAL profile and produces a resolved catalog containing:

  • Only the selected controls (not the entire source catalog)
  • All parameter values set to their resolved values
  • All modifications (additions, alterations) applied
  • A single, self-contained document with no external dependencies

The ProfileResolver accepts file paths, URLs, or document nodes and returns an IDocumentNodeItem containing the resolved catalog:

import dev.metaschema.oscal.lib.OscalBindingContext;
import dev.metaschema.oscal.lib.model.Catalog;
import dev.metaschema.oscal.lib.profile.resolver.ProfileResolver;
import dev.metaschema.core.model.IDocumentNodeItem;

import java.nio.file.Path;

// Resolve a profile directly from a file path
ProfileResolver resolver = new ProfileResolver();
IDocumentNodeItem resolvedDocument = resolver.resolve(Path.of("profile.json"));

// Extract the catalog from the resolved document
Catalog resolvedCatalog = (Catalog) resolvedDocument.getValue();

For profiles that import external resources, configure a document loader:

import dev.metaschema.databind.io.DefaultBoundLoader;

// Get the binding context
OscalBindingContext context = OscalBindingContext.instance();

// Create a document loader with the context
DefaultBoundLoader loader = new DefaultBoundLoader(context);

// Configure resolver with custom loader
ProfileResolver resolver = new ProfileResolver();
resolver.setDocumentLoader(loader);

// Resolve the profile
IDocumentNodeItem resolvedDocument = resolver.resolve(Path.of("profile.json"));
Catalog resolvedCatalog = (Catalog) resolvedDocument.getValue();
import dev.metaschema.databind.io.ISerializer;

// Write the resolved catalog
ISerializer<Catalog> serializer = context.newSerializer(
    Format.JSON, Catalog.class);
serializer.serialize(resolvedCatalog, Path.of("resolved-catalog.json"));
import dev.metaschema.oscal.lib.OscalBindingContext;
import dev.metaschema.oscal.lib.model.Catalog;
import dev.metaschema.oscal.lib.model.Profile;
import dev.metaschema.oscal.lib.profile.resolver.ProfileResolver;
import dev.metaschema.databind.io.Format;
import dev.metaschema.databind.io.IDeserializer;
import dev.metaschema.databind.io.ISerializer;

import java.nio.file.Path;

public class ProfileResolutionExample {

    public static void main(String[] args) throws Exception {
        // Get binding context
        OscalBindingContext context = OscalBindingContext.instance();

        // Load profile
        IDeserializer<Profile> profileReader = context.newDeserializer(
            Format.JSON, Profile.class);
        Profile profile = profileReader.deserialize(
            Path.of("fedramp-high-profile.json"));

        // Resolve
        ProfileResolver resolver = new ProfileResolver();
        Catalog resolved = resolver.resolve(profile);

        // Save result
        ISerializer<Catalog> catalogWriter = context.newSerializer(
            Format.JSON, Catalog.class);
        catalogWriter.serialize(resolved,
            Path.of("fedramp-high-resolved.json"));

        System.out.println("Resolved " +
            resolved.getGroups().stream()
                .flatMap(g -> g.getControls().stream())
                .count() + " controls");
    }
}

Understanding what happens during resolution:

Profile
  │
  ├── 1. Load imported catalogs/profiles (recursive)
  │
  ├── 2. Select controls (include/exclude)
  │
  ├── 3. Apply modifications
  │     ├── Set parameters
  │     ├── Add content
  │     └── Alter existing content
  │
  ├── 4. Merge controls (from multiple imports)
  │
  └── 5. Generate resolved catalog

When profiles import remote catalogs:

import java.net.URI;

// Profile imports: "href": "https://example.com/catalog.json"

// The resolver will fetch remote resources automatically
ProfileResolver resolver = new ProfileResolver();
Catalog resolved = resolver.resolve(profile);

Profiles can import other profiles (chaining):

Base Catalog
    ↓
Profile A (selects controls)
    ↓
Profile B (adds customizations)
    ↓
Profile C (organization-specific)
    ↓
Resolved Catalog

Resolution handles chains automatically:

// Even if profile imports another profile, resolution is handled
ProfileResolver resolver = new ProfileResolver();
Catalog resolved = resolver.resolve(organizationProfile);
import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionException;

try {
    Catalog resolved = resolver.resolve(profile);
} catch (ProfileResolutionException e) {
    System.err.println("Resolution failed: " + e.getMessage());
    // Handle specific resolution errors
} catch (IOException e) {
    System.err.println("Failed to load import: " + e.getMessage());
    // Handle I/O errors
}

After resolution, work with the catalog:

Catalog resolved = resolver.resolve(profile);

// Iterate controls
resolved.getGroups().forEach(group -> {
    System.out.println("Group: " + group.getTitle());

    group.getControls().forEach(control -> {
        System.out.println("  Control: " + control.getId() +
            " - " + control.getTitle());
    });
});

// Find specific control
resolved.getGroups().stream()
    .flatMap(g -> g.getControls().stream())
    .filter(c -> c.getId().equals("ac-1"))
    .findFirst()
    .ifPresent(control -> {
        System.out.println("Found: " + control.getTitle());
    });

The resolver supports various options through the profile structure:

Profile Element Effect on Resolution
import/include-all Include all controls from source
import/include-controls Include specific controls
import/exclude-controls Exclude specific controls
merge/combine How to combine duplicate controls
merge/flat Flatten control hierarchy
modify/set-parameters Set parameter values
modify/alters Modify control content
  1. Cache resolved catalogs - Resolution can be expensive
  2. Handle remote failures - Network requests may fail
  3. Validate after resolution - Ensure result is valid
  4. Check for circular imports - Can cause infinite loops

Continue learning about liboscal-java with these related guides: