API Automation Guide
Learn how to automate OSCAL operations using the REST API with Python, Bash, curl, and TypeScript.
Getting Started
API Base URL
All API endpoints are relative to the base URL:
http://localhost:8090/api
For production deployments, replace localhost:8090 with your actual API host. Production deployments use port 8080.
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
Authentication
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
Navigate to OSCAL Hub and sign in.
- Open your profile
Click your username and select Profile.
- Generate a token
Scroll to Service Account Tokens, enter a name and expiration period (1–3650 days), then click Generate Service Account Token.
- Copy the token
Copy the token immediately — it cannot be retrieved later.
Using the Token
Include the token in the Authorization header:
Authorization: Bearer YOUR_TOKEN_HERE
Security 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 and revoke them immediately if compromised.
Validate Documents
Validate OSCAL documents against their schema to ensure compliance.
Endpoint
POST /api/validate
Request Body
{
"content": "... OSCAL document content as string ...",
"modelType": "catalog",
"format": "JSON",
"fileName": "my-catalog.json"
}
Example: curl
curl -X POST http://localhost:8090/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:8090/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:8090/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
fi
Example: 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:8090/api';
const TOKEN = process.env.OSCAL_API_TOKEN;
const content = fs.readFileSync(filePath, 'utf-8');
const request: ValidationRequest = { content, modelType, format, fileName: filePath };
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 Formats
Convert OSCAL documents between XML, JSON, and YAML formats.
Endpoint
POST /api/convert
Request Body
{
"content": "... OSCAL document content ...",
"fromFormat": "XML",
"toFormat": "JSON",
"modelType": "catalog",
"fileName": "my-catalog.xml"
}
Example: curl
curl -X POST http://localhost:8090/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:8090/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
with open(input_file, 'r') as f:
content = f.read()
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']:
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:8090/api"
TOKEN="${OSCAL_API_TOKEN}"
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
CONTENT=$(cat "$INPUT_FILE" | jq -Rs .)
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\"
}")
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
fi
Example: 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:8090/api';
const TOKEN = process.env.OSCAL_API_TOKEN;
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}`);
const content = fs.readFileSync(inputFile, 'utf-8');
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'}`);
}
}
Resolve Profiles
Resolve OSCAL profiles into fully resolved catalogs.
Endpoint
POST /api/profile/resolve
Request Body
{
"profileContent": "... OSCAL profile content ...",
"format": "JSON"
}
Example: curl
curl -X POST http://localhost:8090/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:8090/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
with open(profile_file, 'r') as f:
profile_content = f.read()
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')
Batch Operations
Process multiple OSCAL files in a single batch operation (validate or convert).
Endpoint
POST /api/batch
Example: 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:8090/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
files = glob.glob(file_pattern)
print(f'Found {len(files)} files to validate')
results = []
for file_path in files:
print(f'\nValidating {file_path}...')
with open(file_path, 'r') as f:
content = f.read()
if file_path.endswith('.xml'):
format = 'XML'
elif file_path.endswith('.json'):
format = 'JSON'
else:
format = 'YAML'
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)')
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}')
return results
# Usage
batch_validate('oscal-files/*.json', 'catalog')
File Management
Manage saved OSCAL files through the API.
List Files
# curl
curl -X GET http://localhost:8090/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
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
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
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")
Error Handling
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:8090/api'
TOKEN = os.getenv('OSCAL_API_TOKEN')
try:
with open(file_path, 'r') as f:
content = f.read()
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
)
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
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')
Best Practices
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)
if response.status_code == 200:
return response
if 400 <= response.status_code < 500:
return response
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
CI/CD Integration
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.json
GitLab 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_requests
Example 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:8090/api')
TOKEN = os.getenv('OSCAL_API_TOKEN')
if not TOKEN:
print('Error: OSCAL_API_TOKEN environment variable not set')
return 1
patterns = ['**/*.json', '**/*.xml', '**/*.yaml']
files = []
for pattern in patterns:
files.extend(glob.glob(pattern, recursive=True))
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')
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()
if file_path.endswith('.xml'):
format = 'XML'
elif file_path.endswith('.json'):
format = 'JSON'
else:
format = 'YAML'
model_type = 'catalog'
if 'profile' in content.lower():
model_type = 'profile'
elif 'system-security-plan' in content.lower():
model_type = 'system-security-plan'
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
with open('validation-results.json', 'w') as f:
json.dump(results, f, indent=2)
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 1 if failed_count > 0 else 0
if __name__ == '__main__':
sys.exit(validate_all_oscal_files())
Additional Resources
- Interactive API Documentation (Swagger UI) — Test API endpoints directly in your browser
- User Guide — Complete guide to using the web interface
- NIST OSCAL Documentation — Official OSCAL documentation and specifications