API Automation Guide
Learn how to automate OSCAL operations using our REST API with Python, Bash, curl, and TypeScript
API Base URL
All API endpoints are relative to the base URL:
http://localhost:8080/apiFor production deployments, replace localhost:8080 with your actual API host.
Available Endpoints
POST /api/validate- Validate OSCAL documentsPOST /api/convert- Convert document formatsPOST /api/profile/resolve- Resolve OSCAL profilesPOST /api/batch- Process multiple filesGET /api/batch/:operationId- Get batch operation statusGET /api/files- List saved filesGET /api/files/:fileId- Get file metadataGET /api/files/:fileId/content- Get file contentPOST /api/files- Upload and save fileDELETE /api/files/:fileId- Delete fileGET /api/health- Health check
OSCAL Model Types
The following model types are supported:
catalogprofilecomponent-definitionsystem-security-planassessment-planassessment-resultsplan-of-action-and-milestones
Supported Formats
JSONXMLYAML
All API requests require authentication using a service account token. You can generate tokens from the Profile page in the web interface.
Creating a Service Account Token
- Log in to the web interface
- Click your username and select "Profile"
- Scroll to "Service Account Tokens"
- Enter a name and expiration period (1-3650 days)
- Click "Generate Service Account Token"
- Copy the token immediately (it cannot be retrieved later)
Using the Token
Include the token in the Authorization header:
Authorization: Bearer YOUR_TOKEN_HERESecurity Best Practices:
- Store tokens in environment variables or secure vaults
- Never commit tokens to version control
- Use different tokens for different environments
- Rotate tokens regularly
- Revoke tokens immediately if compromised
Validate OSCAL documents against their schema to ensure compliance.
Endpoint
POST /api/validateRequest Body
{
"content": "... OSCAL document content as string ...",
"modelType": "catalog",
"format": "JSON",
"fileName": "my-catalog.json"
}Example: curl
curl -X POST http://localhost:8080/api/validate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"content": "{\"catalog\": {\"uuid\": \"123\", \"metadata\": {...}}}",
"modelType": "catalog",
"format": "JSON",
"fileName": "my-catalog.json"
}'Example: Python
import requests
import json
import os
# Load your OSCAL document
with open('my-catalog.json', 'r') as f:
content = f.read()
# API configuration
API_BASE_URL = 'http://localhost:8080/api'
TOKEN = os.getenv('OSCAL_API_TOKEN') # Store token in environment variable
# Validate the document
response = requests.post(
f'{API_BASE_URL}/validate',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'content': content,
'modelType': 'catalog',
'format': 'JSON',
'fileName': 'my-catalog.json'
}
)
result = response.json()
if result['valid']:
print('✓ Document is valid!')
else:
print('✗ Validation failed:')
for error in result['errors']:
print(f" Line {error['line']}: {error['message']}")
print(f" Path: {error['path']}")Example: Bash Script
#!/bin/bash
# Configuration
API_BASE_URL="http://localhost:8080/api"
TOKEN="${OSCAL_API_TOKEN}"
FILE_PATH="my-catalog.json"
# Read file content (escape quotes for JSON)
CONTENT=$(cat "$FILE_PATH" | jq -Rs .)
# Create request body
REQUEST_BODY=$(cat <<EOF
{
"content": $CONTENT,
"modelType": "catalog",
"format": "JSON",
"fileName": "$FILE_PATH"
}
EOF
)
# Validate document
response=$(curl -s -X POST "$API_BASE_URL/validate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "$REQUEST_BODY")
# Check if valid
is_valid=$(echo "$response" | jq -r '.valid')
if [ "$is_valid" = "true" ]; then
echo "✓ Document is valid!"
exit 0
else
echo "✗ Validation failed:"
echo "$response" | jq -r '.errors[] | " Line \(.line): \(.message)"'
exit 1
fiExample: TypeScript
import * as fs from 'fs';
interface ValidationRequest {
content: string;
modelType: string;
format: string;
fileName?: string;
}
interface ValidationError {
line: number;
column: number;
message: string;
severity: string;
path: string;
}
interface ValidationResult {
valid: boolean;
errors: ValidationError[];
fileName: string;
modelType: string;
format: string;
timestamp: string;
}
async function validateDocument(
filePath: string,
modelType: string,
format: string
): Promise<ValidationResult> {
const API_BASE_URL = 'http://localhost:8080/api';
const TOKEN = process.env.OSCAL_API_TOKEN;
// Read file content
const content = fs.readFileSync(filePath, 'utf-8');
// Create request
const request: ValidationRequest = {
content,
modelType,
format,
fileName: filePath
};
// Send validation request
const response = await fetch(`${API_BASE_URL}/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN}`
},
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}
return await response.json() as ValidationResult;
}
// Usage
(async () => {
try {
const result = await validateDocument(
'my-catalog.json',
'catalog',
'JSON'
);
if (result.valid) {
console.log('✓ Document is valid!');
} else {
console.log('✗ Validation failed:');
result.errors.forEach(error => {
console.log(` Line ${error.line}: ${error.message}`);
console.log(` Path: ${error.path}`);
});
}
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
})();Response Format
{
"valid": true,
"errors": [],
"fileName": "my-catalog.json",
"modelType": "catalog",
"format": "JSON",
"timestamp": "2025-10-20T14:30:00Z"
}
// Or if invalid:
{
"valid": false,
"errors": [
{
"line": 15,
"column": 8,
"message": "Missing required field 'uuid'",
"severity": "ERROR",
"path": "/catalog/metadata"
}
],
"fileName": "my-catalog.json",
"modelType": "catalog",
"format": "JSON",
"timestamp": "2025-10-20T14:30:00Z"
}Convert OSCAL documents between XML, JSON, and YAML formats.
Endpoint
POST /api/convertRequest Body
{
"content": "... OSCAL document content ...",
"fromFormat": "XML",
"toFormat": "JSON",
"modelType": "catalog",
"fileName": "my-catalog.xml"
}Example: curl
curl -X POST http://localhost:8080/api/convert \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"content": "<catalog>...</catalog>",
"fromFormat": "XML",
"toFormat": "JSON",
"modelType": "catalog",
"fileName": "my-catalog.xml"
}'Example: Python
import requests
import os
def convert_oscal_format(
input_file: str,
from_format: str,
to_format: str,
model_type: str,
output_file: str
):
"""Convert OSCAL document from one format to another."""
API_BASE_URL = 'http://localhost:8080/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
# Read input file
with open(input_file, 'r') as f:
content = f.read()
# Convert
response = requests.post(
f'{API_BASE_URL}/convert',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'content': content,
'fromFormat': from_format,
'toFormat': to_format,
'modelType': model_type,
'fileName': input_file
}
)
result = response.json()
if result['success']:
# Save converted content
with open(output_file, 'w') as f:
f.write(result['convertedContent'])
print(f'✓ Converted {input_file} from {from_format} to {to_format}')
print(f' Output: {output_file}')
else:
print(f'✗ Conversion failed: {result.get("error", "Unknown error")}')
return False
return True
# Usage
convert_oscal_format(
input_file='catalog.xml',
from_format='XML',
to_format='JSON',
model_type='catalog',
output_file='catalog.json'
)Example: Bash Script
#!/bin/bash
# Convert OSCAL document format
# Usage: ./convert.sh input.xml JSON catalog output.json
INPUT_FILE="$1"
TO_FORMAT="$2"
MODEL_TYPE="$3"
OUTPUT_FILE="$4"
API_BASE_URL="http://localhost:8080/api"
TOKEN="${OSCAL_API_TOKEN}"
# Detect input format from extension
case "$INPUT_FILE" in
*.xml) FROM_FORMAT="XML" ;;
*.json) FROM_FORMAT="JSON" ;;
*.yaml|*.yml) FROM_FORMAT="YAML" ;;
*) echo "Error: Unknown file format"; exit 1 ;;
esac
# Read and escape content
CONTENT=$(cat "$INPUT_FILE" | jq -Rs .)
# Convert
response=$(curl -s -X POST "$API_BASE_URL/convert" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"content\": $CONTENT,
\"fromFormat\": \"$FROM_FORMAT\",
\"toFormat\": \"$TO_FORMAT\",
\"modelType\": \"$MODEL_TYPE\",
\"fileName\": \"$INPUT_FILE\"
}")
# Check success
success=$(echo "$response" | jq -r '.success')
if [ "$success" = "true" ]; then
echo "$response" | jq -r '.convertedContent' > "$OUTPUT_FILE"
echo "✓ Converted $INPUT_FILE from $FROM_FORMAT to $TO_FORMAT"
echo " Output: $OUTPUT_FILE"
else
echo "✗ Conversion failed:"
echo "$response" | jq -r '.error'
exit 1
fiExample: TypeScript
import * as fs from 'fs';
import * as path from 'path';
interface ConversionRequest {
content: string;
fromFormat: string;
toFormat: string;
modelType: string;
fileName?: string;
}
interface ConversionResult {
success: boolean;
convertedContent?: string;
error?: string;
fileName: string;
modelType: string;
originalFormat: string;
convertedFormat: string;
timestamp: string;
}
async function convertFormat(
inputFile: string,
toFormat: string,
modelType: string,
outputFile: string
): Promise<void> {
const API_BASE_URL = 'http://localhost:8080/api';
const TOKEN = process.env.OSCAL_API_TOKEN;
// Detect input format from extension
const ext = path.extname(inputFile).toLowerCase();
const formatMap: Record<string, string> = {
'.xml': 'XML',
'.json': 'JSON',
'.yaml': 'YAML',
'.yml': 'YAML'
};
const fromFormat = formatMap[ext];
if (!fromFormat) {
throw new Error(`Unknown file format: ${ext}`);
}
// Read file
const content = fs.readFileSync(inputFile, 'utf-8');
// Convert
const response = await fetch(`${API_BASE_URL}/convert`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN}`
},
body: JSON.stringify({
content,
fromFormat,
toFormat,
modelType,
fileName: inputFile
} as ConversionRequest)
});
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}
const result = await response.json() as ConversionResult;
if (result.success && result.convertedContent) {
fs.writeFileSync(outputFile, result.convertedContent, 'utf-8');
console.log(`✓ Converted ${inputFile} from ${fromFormat} to ${toFormat}`);
console.log(` Output: ${outputFile}`);
} else {
throw new Error(`Conversion failed: ${result.error || 'Unknown error'}`);
}
}
// Usage
(async () => {
try {
await convertFormat(
'catalog.xml',
'JSON',
'catalog',
'catalog.json'
);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
})();Resolve OSCAL profiles into fully resolved catalogs.
Endpoint
POST /api/profile/resolveRequest Body
{
"profileContent": "... OSCAL profile content ...",
"format": "JSON"
}Example: curl
curl -X POST http://localhost:8080/api/profile/resolve \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"profileContent": "{\"profile\": {...}}",
"format": "JSON"
}'Example: Python
import requests
import os
def resolve_profile(profile_file: str, output_file: str, format: str = 'JSON'):
"""Resolve an OSCAL profile into a catalog."""
API_BASE_URL = 'http://localhost:8080/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
# Read profile
with open(profile_file, 'r') as f:
profile_content = f.read()
# Resolve
response = requests.post(
f'{API_BASE_URL}/profile/resolve',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'profileContent': profile_content,
'format': format
}
)
result = response.json()
if result['success']:
with open(output_file, 'w') as f:
f.write(result['resolvedCatalog'])
print(f'✓ Profile resolved successfully')
print(f' Output: {output_file}')
else:
print(f'✗ Resolution failed: {result.get("error", "Unknown error")}')
return False
return True
# Usage
resolve_profile('my-profile.json', 'resolved-catalog.json', 'JSON')Process multiple OSCAL files in a single batch operation (validate or convert).
Endpoint
POST /api/batchExample: Python Batch Validation
import requests
import os
import glob
def batch_validate(file_pattern: str, model_type: str):
"""Validate multiple OSCAL files in batch."""
API_BASE_URL = 'http://localhost:8080/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
# Find all matching files
files = glob.glob(file_pattern)
print(f'Found {len(files)} files to validate')
# Process each file
results = []
for file_path in files:
print(f'\nValidating {file_path}...')
with open(file_path, 'r') as f:
content = f.read()
# Detect format
if file_path.endswith('.xml'):
format = 'XML'
elif file_path.endswith('.json'):
format = 'JSON'
else:
format = 'YAML'
# Validate
response = requests.post(
f'{API_BASE_URL}/validate',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'content': content,
'modelType': model_type,
'format': format,
'fileName': file_path
}
)
result = response.json()
results.append({
'file': file_path,
'valid': result['valid'],
'errors': result.get('errors', [])
})
if result['valid']:
print(f' ✓ Valid')
else:
print(f' ✗ Invalid ({len(result["errors"])} errors)')
# Summary
print(f'\n{"="*60}')
print('BATCH VALIDATION SUMMARY')
print(f'{"="*60}')
valid_count = sum(1 for r in results if r['valid'])
print(f'Total: {len(results)} files')
print(f'Valid: {valid_count}')
print(f'Invalid: {len(results) - valid_count}')
# Show errors
for result in results:
if not result['valid']:
print(f'\n{result["file"]}:')
for error in result['errors'][:3]: # Show first 3 errors
print(f' Line {error["line"]}: {error["message"]}')
if len(result['errors']) > 3:
print(f' ... and {len(result["errors"]) - 3} more errors')
return results
# Usage
batch_validate('oscal-files/*.json', 'catalog')Manage saved OSCAL files through the API.
List Files
# curl
curl -X GET http://localhost:8080/api/files \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Python
response = requests.get(
f'{API_BASE_URL}/files',
headers={'Authorization': f'Bearer {TOKEN}'}
)
files = response.json()
for file in files:
print(f"{file['id']}: {file['fileName']} ({file['modelType']})")Upload File
# Python
with open('catalog.json', 'r') as f:
content = f.read()
response = requests.post(
f'{API_BASE_URL}/files',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'content': content,
'fileName': 'catalog.json',
'modelType': 'catalog',
'format': 'JSON'
}
)
saved_file = response.json()
print(f"File uploaded: {saved_file['id']}")Get File Content
# Python
file_id = 'abc123'
response = requests.get(
f'{API_BASE_URL}/files/{file_id}/content',
headers={'Authorization': f'Bearer {TOKEN}'}
)
content = response.json()['content']
print(content)Delete File
# Python
file_id = 'abc123'
response = requests.delete(
f'{API_BASE_URL}/files/{file_id}',
headers={'Authorization': f'Bearer {TOKEN}'}
)
if response.status_code == 200:
print(f"File {file_id} deleted successfully")HTTP Status Codes
200 OK- Request successful400 Bad Request- Invalid request parameters401 Unauthorized- Missing or invalid authentication token404 Not Found- Resource not found500 Internal Server Error- Server error
Example: Python Error Handling
import requests
from requests.exceptions import RequestException
def validate_with_error_handling(file_path: str, model_type: str, format: str):
"""Validate OSCAL document with comprehensive error handling."""
API_BASE_URL = 'http://localhost:8080/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
try:
# Read file
with open(file_path, 'r') as f:
content = f.read()
# Validate
response = requests.post(
f'{API_BASE_URL}/validate',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'content': content,
'modelType': model_type,
'format': format,
'fileName': file_path
},
timeout=30 # 30 second timeout
)
# Check for HTTP errors
if response.status_code == 401:
print('Error: Invalid or expired authentication token')
return False
elif response.status_code == 400:
print('Error: Invalid request parameters')
print(response.json())
return False
elif response.status_code != 200:
print(f'Error: API returned status code {response.status_code}')
return False
# Parse result
result = response.json()
if result['valid']:
print(f'✓ {file_path} is valid')
return True
else:
print(f'✗ {file_path} has validation errors:')
for error in result['errors']:
print(f" Line {error['line']}: {error['message']}")
return False
except FileNotFoundError:
print(f'Error: File not found: {file_path}')
return False
except RequestException as e:
print(f'Error: Network request failed: {e}')
return False
except ValueError as e:
print(f'Error: Invalid JSON response: {e}')
return False
except Exception as e:
print(f'Error: Unexpected error: {e}')
return False
# Usage
validate_with_error_handling('catalog.json', 'catalog', 'JSON')Security
- Always store API tokens in environment variables or secure vaults
- Never hardcode tokens in source code
- Use HTTPS in production environments
- Implement token rotation policies
- Use different tokens for different environments (dev, staging, prod)
Performance
- Use batch operations when processing multiple files
- Set appropriate timeouts for API requests
- Implement retry logic with exponential backoff
- Cache validation results when appropriate
- Process large files asynchronously
Reliability
- Always check HTTP status codes before parsing responses
- Implement comprehensive error handling
- Log API requests and responses for debugging
- Validate input data before sending to API
- Handle network timeouts gracefully
Example: Retry Logic with Exponential Backoff
import time
import requests
from typing import Optional
def api_request_with_retry(
url: str,
method: str = 'POST',
max_retries: int = 3,
**kwargs
) -> Optional[requests.Response]:
"""Make API request with exponential backoff retry."""
for attempt in range(max_retries):
try:
response = requests.request(method, url, **kwargs)
# Success - return response
if response.status_code == 200:
return response
# Don't retry client errors (400-499)
if 400 <= response.status_code < 500:
return response
# Retry server errors (500-599)
if response.status_code >= 500:
wait_time = 2 ** attempt # 1s, 2s, 4s
print(f'Server error, retrying in {wait_time}s...')
time.sleep(wait_time)
continue
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise
wait_time = 2 ** attempt
print(f'Request failed, retrying in {wait_time}s...')
time.sleep(wait_time)
return None
# Usage
response = api_request_with_retry(
f'{API_BASE_URL}/validate',
method='POST',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json=request_body,
timeout=30
)Integrate OSCAL validation and processing into your continuous integration and deployment pipelines.
GitHub Actions Example
name: Validate OSCAL Documents
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install requests
- name: Validate OSCAL files
env:
OSCAL_API_TOKEN: ${{ secrets.OSCAL_API_TOKEN }}
OSCAL_API_URL: ${{ secrets.OSCAL_API_URL }}
run: |
python scripts/validate_all.py
- name: Upload validation results
if: always()
uses: actions/upload-artifact@v3
with:
name: validation-results
path: validation-results.jsonGitLab CI Example
validate-oscal:
image: python:3.11
stage: test
before_script:
- pip install requests
script:
- python scripts/validate_all.py
variables:
OSCAL_API_TOKEN: $OSCAL_API_TOKEN
OSCAL_API_URL: $OSCAL_API_URL
artifacts:
reports:
junit: validation-results.xml
paths:
- validation-results.json
when: always
only:
- main
- merge_requestsExample Validation Script for CI/CD
#!/usr/bin/env python3
"""
validate_all.py - Validate all OSCAL files in repository for CI/CD
"""
import os
import sys
import glob
import json
import requests
from pathlib import Path
def validate_all_oscal_files():
"""Validate all OSCAL files and return exit code."""
API_BASE_URL = os.getenv('OSCAL_API_URL', 'http://localhost:8080/api')
TOKEN = os.getenv('OSCAL_API_TOKEN')
if not TOKEN:
print('Error: OSCAL_API_TOKEN environment variable not set')
return 1
# Find all OSCAL files
patterns = ['**/*.json', '**/*.xml', '**/*.yaml']
files = []
for pattern in patterns:
files.extend(glob.glob(pattern, recursive=True))
# Filter to only OSCAL files (check for common OSCAL keywords)
oscal_files = []
for file_path in files:
with open(file_path, 'r') as f:
content = f.read()
if any(kw in content for kw in ['catalog', 'profile', 'component-definition',
'system-security-plan', 'assessment-plan']):
oscal_files.append(file_path)
print(f'Found {len(oscal_files)} OSCAL files to validate')
# Validate each file
results = []
failed_count = 0
for file_path in oscal_files:
print(f'\nValidating {file_path}...')
with open(file_path, 'r') as f:
content = f.read()
# Detect format and model type
if file_path.endswith('.xml'):
format = 'XML'
elif file_path.endswith('.json'):
format = 'JSON'
else:
format = 'YAML'
# Detect model type (simplified)
model_type = 'catalog' # Default
if 'profile' in content.lower():
model_type = 'profile'
elif 'system-security-plan' in content.lower():
model_type = 'system-security-plan'
# Validate
try:
response = requests.post(
f'{API_BASE_URL}/validate',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {TOKEN}'
},
json={
'content': content,
'modelType': model_type,
'format': format,
'fileName': file_path
},
timeout=60
)
if response.status_code != 200:
print(f' ✗ API error: {response.status_code}')
failed_count += 1
continue
result = response.json()
results.append({
'file': file_path,
'valid': result['valid'],
'errors': result.get('errors', [])
})
if result['valid']:
print(f' ✓ Valid')
else:
print(f' ✗ Invalid ({len(result["errors"])} errors)')
for error in result['errors'][:3]:
print(f' Line {error["line"]}: {error["message"]}')
failed_count += 1
except Exception as e:
print(f' ✗ Error: {e}')
failed_count += 1
# Save results
with open('validation-results.json', 'w') as f:
json.dump(results, f, indent=2)
# Summary
print(f'\n{"="*60}')
print('VALIDATION SUMMARY')
print(f'{"="*60}')
print(f'Total files: {len(oscal_files)}')
print(f'Valid: {len(oscal_files) - failed_count}')
print(f'Invalid: {failed_count}')
# Return exit code
return 1 if failed_count > 0 else 0
if __name__ == '__main__':
sys.exit(validate_all_oscal_files())Test API endpoints directly in your browser with the interactive Swagger UI
User GuideComplete guide to using the web interface
NIST OSCAL DocumentationOfficial OSCAL documentation and specifications