Complete API reference for ansible-inspec library and CLI.
Latest Update (v0.2.6): Version bump with documentation improvements.
v0.2.5: Fixed critical missing import statements that caused NameError in v0.2.4. All translator files now properly import re module for variable name sanitization.
v0.2.4: Added missing error handling and result tracking to assertion tasks. All assert tasks now include ignore_errors: True and register fields, enabling full compliance scans without early abortion and proper result collection for reporting.
v0.2.2: Added dynamic custom resource mapper that automatically translates custom InSpec resources to native Ansible modules. Also fixed InSpec parser value extraction to properly handle operators in assertions.
v0.2.1: Fixed critical translator field mismatch that prevented native translation.
v0.1.6: Fixed critical converter bug that caused 99% control loss when converting profiles with quoted control IDs.
- Installation
- CLI Reference
- REST API Server
- Python API
- Core Classes
- Reporters
- Converters
- Bug Fixes and Testing
- Examples
# Install latest version
pip install ansible-inspec
# Install specific version
pip install ansible-inspec==0.1.0
# Install with development dependencies
pip install ansible-inspec[dev]# Pull latest image
docker pull htunnthuthu/ansible-inspec:latest
# Pull specific version
docker pull htunnthuthu/ansible-inspec:0.1.0
# Run container
docker run --rm htunnthuthu/ansible-inspec:latest --helpSee Docker Usage Guide for detailed Docker instructions.
ansible-inspec [command] [options]Global Flags:
--version- Show version and exit--license- Show license information--help, -h- Show help message
Run InSpec profiles on target systems using Ansible inventory.
ansible-inspec exec PROFILE_PATH [options]Arguments:
PROFILE_PATH- Path to InSpec profile directory
Options:
-
-t, --target TARGET- Target specification (inventory file, host, or special target)- File path:
/path/to/inventory.yml - Single host:
user@hostname - Local:
local:// - Docker:
docker://container_name
- File path:
-
--reporter REPORTER- Output format for results- Single:
json,html,junit,cli - Multiple:
json:path.json html:path.html - Default:
cli
- Single:
-
--output PATH- Output file path for reports -
--controls CONTROLS- Comma-separated list of control IDs to run -
--tags TAGS- Comma-separated list of tags to filter controls
Examples:
# Run profile on local system
ansible-inspec exec my-profile --target local://
# Run on remote host via SSH
ansible-inspec exec my-profile --target user@hostname
# Run with Ansible inventory
ansible-inspec exec my-profile --target inventory.yml
# Generate JSON report
ansible-inspec exec my-profile --target local:// --reporter json --output report.json
# Multiple reporters
ansible-inspec exec my-profile --target local:// \
--reporter "json:report.json html:report.html"
# Run specific controls
ansible-inspec exec my-profile --target local:// --controls "ssh-1,ssh-2"
# Filter by tags
ansible-inspec exec my-profile --target local:// --tags "critical,security"Transform Ruby-based InSpec profiles into pure Ansible collections.
ansible-inspec convert PROFILE_PATH [options]Arguments:
PROFILE_PATH- Path to InSpec profile directory or Supermarket profile name
Options:
-
-o, --output-dir DIR- Output directory for converted collection- Default:
./collections
- Default:
-
-n, --namespace NAME- Ansible Galaxy namespace- Default:
compliance
- Default:
-
-c, --collection-name NAME- Collection name- Default: Derived from profile name
-
--no-build- Skip building the collection tarball -
--force- Overwrite existing collection -
--include-callback- Bundle compliance reporter callback plugin (enabled by default)
Examples:
# Basic conversion
ansible-inspec convert my-profile
# Custom namespace and collection name
ansible-inspec convert my-profile \
--namespace myorg \
--collection-name security_baseline
# Specify output directory
ansible-inspec convert my-profile --output-dir /path/to/collections
# Convert and build tarball
ansible-inspec convert my-profile --output-dir ./dist
# Download and convert from Chef Supermarket
ansible-inspec convert dev-sec/linux-baselineSearch and download compliance profiles from Chef Supermarket.
ansible-inspec supermarket SUBCOMMAND [options]Subcommands:
ansible-inspec supermarket search [QUERY] [options]Options:
-l, --limit N- Limit results (default: 20)-s, --sort FIELD- Sort by:name,downloads,updated(default:updated)
Examples:
# Search all profiles
ansible-inspec supermarket search
# Search for CIS profiles
ansible-inspec supermarket search CIS
# Search and limit results
ansible-inspec supermarket search linux --limit 10
# Sort by downloads
ansible-inspec supermarket search --sort downloadsansible-inspec supermarket info PROFILE_NAMEExamples:
# Get info about a profile
ansible-inspec supermarket info dev-sec/linux-baseline
# Get info about CIS benchmark
ansible-inspec supermarket info cis/cis-ubuntu-20-04-server-level1ansible-inspec supermarket download PROFILE_NAME [options]Options:
-o, --output-dir DIR- Download destination (default:./profiles)
Examples:
# Download profile
ansible-inspec supermarket download dev-sec/linux-baseline
# Download to specific directory
ansible-inspec supermarket download dev-sec/linux-baseline \
--output-dir /tmp/profilesThe ansible-inspec server provides a production-ready REST API for managing compliance testing workflows, VCS integration, and user authentication.
http://localhost:8080/api/v1
# Using Docker Compose (recommended)
docker-compose up -d
# Using Python directly
uvicorn ansible_inspec.server.api:app --host 0.0.0.0 --port 8080
# With environment variables
export DATABASE_URL="postgresql://user:pass@localhost:5432/ansibleinspec"
export JWT_SECRET="your-secret-key"
export ENCRYPTION_KEY="your-encryption-key"
uvicorn ansible_inspec.server.api:app --reloadThe API supports two authentication methods:
- Azure AD OAuth2 - Enterprise SSO (recommended for production)
- Password-based - Local username/password authentication
All endpoints (except health check and auth endpoints) require authentication via JWT bearer tokens.
Authorization Header:
Authorization: Bearer <jwt_token>
- admin - Full access to all endpoints including user management
- operator - Can manage job templates, jobs, and VCS repositories
- viewer - Read-only access to jobs and templates
Health check endpoint (no authentication required).
Response:
{
"status": "healthy",
"version": "0.2.6",
"storage_backend": "database",
"database": "connected",
"auth_enabled": true,
"vcs_enabled": true
}API information endpoint.
Response:
{
"name": "Ansible-InSpec API",
"version": "0.2.6",
"endpoints": {
"job_templates": "/api/v1/job-templates",
"jobs": "/api/v1/jobs",
"workflows": "/api/v1/workflows",
"users": "/api/v1/users",
"vcs_credentials": "/api/v1/vcs/credentials",
"vcs_repositories": "/api/v1/vcs/repositories",
"auth": "/api/v1/auth"
}
}Redirect to Azure AD OAuth2 login page.
Response:
- 302 Redirect to Microsoft login page
Login with username and password.
Request Body:
{
"username": "admin",
"password": "your-password"
}Response:
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 3600,
"user": {
"id": "user-123",
"username": "admin",
"email": "admin@example.com",
"roles": ["admin"]
}
}Status Codes:
- 200: Success
- 401: Invalid credentials
OAuth2 callback endpoint for Azure AD authentication.
Query Parameters:
code- Authorization code from Azure ADstate- State parameter for CSRF protection
Response:
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"token_type": "bearer",
"expires_in": 3600
}Get current authenticated user information.
Headers:
Authorization: Bearer <token>
Response:
{
"id": "user-123",
"username": "admin",
"email": "admin@example.com",
"name": "Administrator",
"roles": ["admin"],
"active": true,
"last_login": "2026-02-11T10:30:00Z"
}Status Codes:
- 200: Success
- 401: Unauthorized
Logout current user and invalidate token.
Response:
{
"message": "Logged out successfully"
}List all job templates.
Query Parameters:
limit(int, optional): Maximum results (default: 100)offset(int, optional): Pagination offset (default: 0)
Required Role: viewer
Response:
{
"count": 2,
"results": [
{
"id": "template-123",
"name": "linux-baseline",
"description": "DevSec Linux Baseline",
"profile": "dev-sec/linux-baseline",
"extra_vars": {},
"vcs_repo_id": null,
"created_at": "2026-02-11T10:00:00Z",
"updated_at": "2026-02-11T10:00:00Z"
}
]
}Create a new job template.
Required Role: operator
Request Body:
{
"name": "linux-baseline",
"description": "DevSec Linux Baseline compliance check",
"profile": "dev-sec/linux-baseline",
"extra_vars": {
"check_level": "critical"
},
"vcs_repo_id": "repo-456",
"vcs_path": "profiles/linux-baseline",
"vcs_sync": true
}Response:
{
"id": "template-123",
"name": "linux-baseline",
"description": "DevSec Linux Baseline compliance check",
"profile": "dev-sec/linux-baseline",
"extra_vars": {"check_level": "critical"},
"created_at": "2026-02-11T10:00:00Z"
}Status Codes:
- 201: Created
- 400: Invalid input
- 401: Unauthorized
- 403: Forbidden
Get a specific job template by ID.
Required Role: viewer
Response:
{
"id": "template-123",
"name": "linux-baseline",
"description": "DevSec Linux Baseline",
"profile": "dev-sec/linux-baseline",
"extra_vars": {},
"vcs_repo_id": null,
"vcs_path": null,
"vcs_sync": false,
"created_at": "2026-02-11T10:00:00Z",
"updated_at": "2026-02-11T10:00:00Z"
}Status Codes:
- 200: Success
- 404: Template not found
Update a job template.
Required Role: operator
Request Body:
{
"name": "updated-name",
"description": "Updated description",
"profile": "new-profile-path",
"extra_vars": {"new_var": "value"}
}Response:
{
"id": "template-123",
"name": "updated-name",
"description": "Updated description",
"updated_at": "2026-02-11T11:00:00Z"
}Status Codes:
- 200: Success
- 404: Template not found
- 400: Invalid input
Delete a job template.
Required Role: admin
Status Codes:
- 204: No Content (success)
- 404: Template not found
Launch a job from a template.
Required Role: operator
Request Body:
{
"target": "production-servers",
"extra_vars": {
"override_var": "value"
}
}Response:
{
"id": "job-789",
"template_id": "template-123",
"status": "running",
"created_at": "2026-02-11T11:00:00Z"
}Status Codes:
- 201: Created
- 404: Template not found
List all jobs with optional filtering.
Query Parameters:
limit(int): Maximum results (default: 100)offset(int): Pagination offset (default: 0)status(string): Filter by status (pending, running, completed, failed)template_id(string): Filter by template ID
Required Role: viewer
Response:
{
"count": 5,
"results": [
{
"id": "job-789",
"template_id": "template-123",
"status": "completed",
"target": "production-servers",
"started_at": "2026-02-11T11:00:00Z",
"finished_at": "2026-02-11T11:05:00Z",
"duration_seconds": 300,
"controls_passed": 45,
"controls_failed": 3,
"controls_total": 48
}
]
}Create and execute a new job.
Required Role: operator
Request Body:
{
"template_id": "template-123",
"target": "production-servers",
"extra_vars": {
"custom_var": "value"
}
}Response:
{
"id": "job-789",
"template_id": "template-123",
"status": "running",
"created_at": "2026-02-11T11:00:00Z"
}Status Codes:
- 201: Created
- 404: Template not found
Get detailed job information and results.
Required Role: viewer
Response:
{
"id": "job-789",
"template_id": "template-123",
"template_name": "linux-baseline",
"status": "completed",
"target": "production-servers",
"started_at": "2026-02-11T11:00:00Z",
"finished_at": "2026-02-11T11:05:00Z",
"duration_seconds": 300,
"result": {
"controls_passed": 45,
"controls_failed": 3,
"controls_skipped": 0,
"controls_total": 48,
"compliance_percentage": 93.75,
"controls": [
{
"id": "ssh-1",
"title": "SSH Protocol Version",
"status": "passed",
"impact": 1.0,
"results": []
}
]
}
}Status Codes:
- 200: Success
- 404: Job not found
Create a new VCS credential for repository authentication.
Required Role: admin
Request Body:
{
"name": "github-token",
"vcs_type": "github",
"token": "ghp_xxxxxxxxxxxx",
"repository_url": "https://github.com/org/repo"
}Alternative with SSH:
{
"name": "gitlab-ssh",
"vcs_type": "gitlab",
"ssh_private_key": "-----BEGIN PRIVATE KEY-----\n...",
"repository_url": "git@gitlab.com:org/repo.git"
}Alternative with username/password:
{
"name": "bitbucket-creds",
"vcs_type": "bitbucket",
"username": "user@example.com",
"password": "app-password",
"repository_url": "https://bitbucket.org/org/repo"
}Response:
{
"id": "cred-456",
"name": "github-token",
"vcs_type": "github",
"username": null
}Note: Credentials are encrypted at rest using Fernet symmetric encryption.
Status Codes:
- 201: Created
- 501: Encryption not configured
List all VCS credentials (without showing sensitive data).
Required Role: operator
Response:
{
"count": 2,
"results": [
{
"id": "cred-456",
"name": "github-token", "vcs_type": "github",
"username": null
},
{
"id": "cred-789",
"name": "gitlab-ssh",
"vcs_type": "gitlab",
"username": "git"
}
]
}Delete a VCS credential.
Required Role: admin
Status Codes:
- 204: No Content (success)
- 404: Credential not found
Register a VCS repository for automatic profile synchronization.
Required Role: admin
Request Body:
{
"name": "compliance-profiles",
"url": "https://github.com/org/compliance-profiles.git",
"branch": "main",
"credential_id": "cred-456",
"poll_interval": 300,
"profile_path": "inspec",
"auto_import": true
}Response:
{
"id": "repo-123",
"name": "compliance-profiles",
"url": "https://github.com/org/compliance-profiles.git",
"branch": "main",
"auto_import": true
}Status Codes:
- 201: Created
- 400: Repository already exists
- 501: VCS not enabled
List all registered VCS repositories.
Required Role: operator
Response:
{
"count": 1,
"results": [
{
"id": "repo-123",
"name": "compliance-profiles",
"url": "https://github.com/org/compliance-profiles.git",
"branch": "main",
"sync_status": "completed",
"last_sync_at": "2026-02-11T10:00:00Z",
"last_commit": "abc123def456",
"auto_import": true
}
]
}Manually trigger repository synchronization.
Required Role: operator
Response:
{
"status": "success",
"message": "Repository sync triggered",
"sync_id": "sync-789"
}Status Codes:
- 200: Success
- 404: Repository not found
- 501: VCS not enabled
Delete a VCS repository configuration (does not delete synced profiles).
Required Role: admin
Status Codes:
- 204: No Content (success)
- 404: Repository not found
Get synchronization history for a repository.
Query Parameters:
limit(int): Maximum results (default: 50)
Required Role: viewer
Response:
{
"repository": "compliance-profiles",
"count": 10,
"results": [
{
"id": "sync-789",
"syncStartedAt": "2026-02-11T10:00:00Z",
"syncCompletedAt": "2026-02-11T10:02:00Z",
"status": "completed",
"commitHash": "abc123def456",
"profilesDiscovered": 5,
"templatesCreated": 5,
"errors": null,
"triggeredBy": "admin",
"triggerType": "manual",
"duration_seconds": 120
}
]
}List all files in a synced repository.
Required Role: viewer
Response:
{
"repository": "compliance-profiles",
"count": 25,
"files": [
"README.md",
"inspec/linux-baseline/inspec.yml",
"inspec/linux-baseline/controls/01_baseline.rb",
"inspec/windows-baseline/inspec.yml"
]
}Status Codes:
- 200: Success
- 400: Repository not synced yet
- 404: Repository not found
Get content of a specific file from a synced repository.
Required Role: viewer
Response:
{
"repository": "compliance-profiles",
"file_path": "inspec/linux-baseline/inspec.yml",
"content": "name: linux-baseline\ntitle: DevSec Linux Baseline\nversion: 2.8.0\n...",
"size": 512
}Status Codes:
- 200: Success
- 400: Invalid file path or binary file
- 404: File not found
GitHub webhook handler for push events.
Headers:
X-Hub-Signature-256: sha256=<signature>
X-GitHub-Event: push
Request Body: GitHub webhook payload
Response:
{
"message": "Repository sync triggered",
"status": "success",
"repository": "compliance-profiles",
"commit": "abc123def456"
}Status Codes:
- 200: Success
- 401: Invalid signature
- 501: Webhooks not enabled
Configuration:
- Go to repository Settings → Webhooks
- Add webhook URL:
http://your-server:8080/api/v1/webhooks/github/{repo_name} - Content type:
application/json - Secret: Set
WEBHOOK_SECRETenvironment variable - Events: Select "Just the push event"
GitLab webhook handler for push events.
Headers:
X-Gitlab-Token: <secret>
Request Body: GitLab webhook payload
Response:
{
"message": "Repository sync triggered",
"status": "success",
"repository": "compliance-profiles",
"commit": "abc123def456"
}Status Codes:
- 200: Success
- 401: Invalid token
- 501: Webhooks not enabled
Configuration:
- Go to repository Settings → Webhooks
- Add webhook URL:
http://your-server:8080/api/v1/webhooks/gitlab/{repo_name} - Secret token: Set
WEBHOOK_SECRETenvironment variable - Trigger: Select "Push events"
List all users (admin only).
Required Role: admin
Response:
{
"count": 3,
"results": [
{
"id": "user-123",
"username": "admin",
"email": "admin@example.com",
"name": "Administrator",
"roles": ["admin"],
"active": true,
"last_login": "2026-02-11T10:00:00Z"
}
]
}Update user roles and status (admin only).
Required Role: admin
Request Body:
{
"roles": ["operator", "viewer"],
"active": true
}Response:
{
"id": "user-123",
"username": "operator1",
"roles": ["operator", "viewer"],
"active": true
}Status Codes:
- 200: Success
- 404: User not found
Get hybrid storage validation status (admin only, hybrid mode only).
Required Role: admin
Response:
{
"mode": "validation",
"validation_days_remaining": 25,
"validation_period_days": 30,
"auto_cutover_enabled": true,
"operations_validated": 1250,
"validation_failures": 0,
"last_validation_check": "2026-02-11T10:00:00Z"
}Status Codes:
- 200: Success
- 400: Not using hybrid storage
Prometheus metrics endpoint (no authentication required).
Response: Prometheus text format
# HELP ansible_inspec_storage_operations_total Total number of storage operations
# TYPE ansible_inspec_storage_operations_total counter
ansible_inspec_storage_operations_total{backend="database",operation="save"} 145
ansible_inspec_storage_operations_total{backend="database",operation="get"} 892
# HELP ansible_inspec_storage_operation_duration_seconds Storage operation duration
# TYPE ansible_inspec_storage_operation_duration_seconds histogram
ansible_inspec_storage_operation_duration_seconds_bucket{backend="database",operation="save",le="0.1"} 142
# HELP ansible_inspec_auth_requests_total Total authentication requests
# TYPE ansible_inspec_auth_requests_total counter
ansible_inspec_auth_requests_total{provider="azure_ad",status="success"} 45
# 1. Login
curl -X POST http://localhost:8080/api/v1/auth/password-login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}' | jq -r '.access_token' > token.txt
TOKEN=$(cat token.txt)
# 2. Create VCS credential
curl -X POST http://localhost:8080/api/v1/vcs/credentials \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "github-token",
"vcs_type": "github",
"token": "ghp_xxxxxxxxxxxx"
}'
# 3. Register repository
curl -X POST http://localhost:8080/api/v1/vcs/repositories \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "compliance-profiles",
"url": "https://github.com/org/compliance-profiles.git",
"branch": "main",
"credential_id": "cred-456",
"auto_import": true
}'
# 4. Trigger manual sync
curl -X POST http://localhost:8080/api/v1/vcs/repositories/compliance-profiles/sync \
-H "Authorization: Bearer $TOKEN"
# 5. List job templates (auto-created from sync)
curl -X GET http://localhost:8080/api/v1/job-templates \
-H "Authorization: Bearer $TOKEN"
# 6. Launch job
curl -X POST http://localhost:8080/api/v1/job-templates/template-123/launch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target": "production-servers",
"extra_vars": {}
}'
# 7. Check job status
curl -X GET http://localhost:8080/api/v1/jobs/job-789 \
-H "Authorization: Bearer $TOKEN"
# 8. View job results
curl -X GET http://localhost:8080/api/v1/jobs/job-789 \
-H "Authorization: Bearer $TOKEN" | jq '.result'import requests
class AnsibleInSpecClient:
def __init__(self, base_url, username, password):
self.base_url = base_url
self.token = None
self.login(username, password)
def login(self, username, password):
"""Login and store token"""
response = requests.post(
f"{self.base_url}/api/v1/auth/password-login",
json={"username": username, "password": password}
)
response.raise_for_status()
self.token = response.json()["access_token"]
def _headers(self):
return {"Authorization": f"Bearer {self.token}"}
def create_job_template(self, name, profile, description=""):
"""Create a job template"""
response = requests.post(
f"{self.base_url}/api/v1/job-templates",
headers=self._headers(),
json={
"name": name,
"profile": profile,
"description": description
}
)
response.raise_for_status()
return response.json()
def launch_job(self, template_id, target="localhost"):
"""Launch a job from template"""
response = requests.post(
f"{self.base_url}/api/v1/job-templates/{template_id}/launch",
headers=self._headers(),
json={"target": target}
)
response.raise_for_status()
return response.json()
def get_job(self, job_id):
"""Get job details"""
response = requests.get(
f"{self.base_url}/api/v1/jobs/{job_id}",
headers=self._headers()
)
response.raise_for_status()
return response.json()
# Usage
client = AnsibleInSpecClient(
"http://localhost:8080",
username="admin",
password="secret"
)
# Create template
template = client.create_job_template(
name="linux-baseline",
profile="dev-sec/linux-baseline",
description="DevSec Linux Baseline"
)
# Launch job
job = client.launch_job(template["id"], target="production-servers")
# Check results
result = client.get_job(job["id"])
print(f"Status: {result['status']}")
print(f"Compliance: {result['result']['compliance_percentage']}%")from ansible_inspec import Runner, ProfileConverter
from ansible_inspec.core import ExecutionConfig
# Execute InSpec profile
config = ExecutionConfig(
profile_path="/path/to/profile",
target="local://",
reporter="json",
output_path="report.json"
)
runner = Runner(config)
result = runner.run()
# Save reports
result.save("report.json", format="json")
result.save("report.html", format="html")
# Convert profile to Ansible collection
converter = ProfileConverter(
profile_path="/path/to/profile",
output_dir="./collections",
namespace="myorg",
collection_name="security"
)
collection_path = converter.convert()
print(f"Collection created at: {collection_path}")Configuration for InSpec profile execution.
from ansible_inspec.core import ExecutionConfig
config = ExecutionConfig(
profile_path: str, # Path to InSpec profile
target: str, # Target (inventory, host, or special)
reporter: str = "cli", # Output format
output_path: Optional[str] = None, # Output file path
controls: Optional[List[str]] = None, # Control IDs to run
tags: Optional[List[str]] = None # Tags to filter
)Attributes:
profile_path(str): Path to InSpec profile directorytarget(str): Target specificationreporter(str): Output format (json, html, junit, cli)output_path(Optional[str]): Path for report outputcontrols(Optional[List[str]]): List of control IDstags(Optional[List[str]]): List of tags to filter
Results from InSpec profile execution.
from ansible_inspec.core import ExecutionResult
result = runner.run() # Returns ExecutionResult
# Properties
result.success # bool: Overall success
result.exit_code # int: Exit code
result.output # str: Raw output
result.error # Optional[str]: Error message
result.controls_passed # int: Number of passed controls
result.controls_failed # int: Number of failed controls
result.controls_total # int: Total controls
# Methods
result.to_json() -> str
result.to_html() -> str
result.to_junit() -> str
result.save(path: str, format: str = "json") -> NoneExample:
# Check results
if result.success:
print(f"All {result.controls_total} controls passed!")
else:
print(f"Failed: {result.controls_failed}/{result.controls_total}")
# Generate reports
json_data = result.to_json()
html_report = result.to_html()
# Save to files
result.save("compliance-report.json", format="json")
result.save("compliance-report.html", format="html")
result.save("compliance-report.xml", format="junit")Execute InSpec profiles.
from ansible_inspec import Runner
from ansible_inspec.core import ExecutionConfig
# Create runner
config = ExecutionConfig(profile_path="./my-profile", target="local://")
runner = Runner(config)
# Execute
result = runner.run()
# Access results
print(f"Success: {result.success}")
print(f"Controls: {result.controls_passed}/{result.controls_total}")Methods:
run() -> ExecutionResult- Execute the profile and return results
Convert InSpec profiles to Ansible collections.
from ansible_inspec import ProfileConverter
converter = ProfileConverter(
profile_path: str,
output_dir: str = "./collections",
namespace: str = "compliance",
collection_name: Optional[str] = None,
force: bool = False,
build: bool = True
)
# Convert profile
collection_path = converter.convert()
print(f"Collection: {collection_path}")Parameters:
profile_path(str): Path to InSpec profileoutput_dir(str): Output directory for collectionnamespace(str): Ansible Galaxy namespacecollection_name(Optional[str]): Collection name (auto-generated if None)force(bool): Overwrite existing collectionbuild(bool): Build collection tarball
Methods:
convert() -> str- Convert profile, returns collection pathbuild() -> str- Build collection tarball, returns tarball path
Example:
# Convert with custom settings
converter = ProfileConverter(
profile_path="/path/to/cis-benchmark",
output_dir="./ansible-collections",
namespace="security",
collection_name="cis_ubuntu_20_04",
force=True,
build=True
)
try:
collection_path = converter.convert()
print(f"✓ Collection created: {collection_path}")
# Build tarball
tarball = converter.build()
print(f"✓ Tarball: {tarball}")
except Exception as e:
print(f"✗ Conversion failed: {e}")Generate InSpec-compatible JSON reports.
from ansible_inspec.reporters import InSpecJSONReport, InSpecControl, InSpecProfile
# Create report
report = InSpecJSONReport(version="5.22.0")
# Add profile
profile = InSpecProfile(
name="my-profile",
title="My Security Profile",
version="1.0.0",
summary="Security baseline",
controls=[]
)
report.profiles.append(profile)
# Add control
control = InSpecControl(
id="ctrl-1",
title="SSH Configuration",
desc="Ensure SSH is configured securely",
impact=0.7,
tags={},
code="describe file('/etc/ssh/sshd_config') { it { should exist } }",
source_location={"ref": "controls/ssh.rb", "line": 1},
results=[{
"status": "passed",
"code_desc": "File /etc/ssh/sshd_config should exist",
"run_time": 0.002
}]
)
profile.controls.append(control)
# Generate JSON
json_output = report.to_json(indent=2)
print(json_output)
# Save to file
report.save("/path/to/report.json")Classes:
Main report container.
Attributes:
version(str): InSpec versionprofiles(List[InSpecProfile]): List of profilesplatform(InSpecPlatform): Platform informationstatistics(InSpecStatistics): Execution statisticserrors(List[str]): Execution errors
Methods:
to_dict() -> Dict- Convert to dictionaryto_json(indent: int = 2) -> str- Generate JSON stringsave(path: str, indent: int = 2) -> None- Save to file
Represents a single control.
Attributes:
id(str): Control IDtitle(str): Control titledesc(str): Descriptionimpact(float): Impact score (0.0 - 1.0)tags(Dict): Tagscode(str): Control codesource_location(Dict): Source file locationresults(List[Dict]): Test results
Represents an InSpec profile.
Attributes:
name(str): Profile nametitle(str): Profile titleversion(str): Versionsummary(str): Summarycontrols(List[InSpecControl]): Controls
Platform information.
Attributes:
name(str): Platform namerelease(str): Release versiontarget_id(str): Target identifier
Execution statistics.
Attributes:
duration(float): Execution duration in secondscontrols(Dict): Control counts
Generate interactive HTML reports.
from ansible_inspec.core import ExecutionResult
result = runner.run()
# Generate HTML
html_report = result.to_html()
# Save HTML report
result.save("compliance-report.html", format="html")HTML Features:
- Interactive dashboard
- Control filtering
- Pass/fail statistics
- Color-coded results
- Execution error section
- Responsive design
Generate JUnit XML for CI/CD integration.
# Generate JUnit XML
junit_xml = result.to_junit()
# Save for CI/CD
result.save("test-results.xml", format="junit")JUnit Features:
- Test suite per profile
- Test case per control
- Failure details
- Execution timing
- CI/CD compatible
The converter translates InSpec profiles (Ruby DSL) into Ansible collections with native tasks.
Key Features (v0.1.6):
- Fixed Critical Bug: Control ID regex now properly handles quotes in control IDs
- Previous versions failed on CIS benchmark controls like
"1.1.1 (L1) Ensure 'password history' is set" - Now successfully converts all 358 controls instead of only 4 (99% improvement)
- Previous versions failed on CIS benchmark controls like
- Sanitized Variable Names: Automatically converts control IDs to valid Ansible variable names
- Example:
"2.2.27 (L1) Ensure..."→"inspec_2_2_27_L1_Ensure"
- Example:
- Custom Resources: Detects and converts InSpec custom resources to Ansible modules
- Native Modules: Prefers Ansible native modules when available (file, service, package, etc.)
from ansible_inspec.converter import ProfileConverter, InSpecControlParser
# Parse InSpec controls with proper quote handling
control_code = '''
control "1.1.1 (L1) Ensure 'Enforce password history' is set to '7 password(s)'" do
impact 1.0
title "Password History"
desc "Enforce password history"
describe registry_key('HKEY_LOCAL_MACHINE\\...') do
its('PasswordHistorySize') { should cmp >= 7 }
end
end
'''
parser = InSpecControlParser(control_code)
controls = parser.parse()
# Successfully extracts full control ID with quotes
assert controls[0]['id'] == "1.1.1 (L1) Ensure 'Enforce password history' is set to '7 password(s)'"
# Analyze full profile
converter = ProfileConverter(profile_path="./cis-benchmark")
profile_info = converter.analyze()
print(f"Profile: {profile_info['name']}")
print(f"Controls: {len(profile_info['controls'])}") # Now gets all controls!
print(f"Custom Resources: {profile_info['custom_resources']}")The converter properly handles complex control IDs from compliance frameworks:
from ansible_inspec.converter import sanitize_variable_name
# Real-world CIS benchmark control IDs
control_ids = [
"1.1.1 (L1) Ensure 'Enforce password history' is set to '7 password(s)'",
"2.2.27 (L1) Ensure 'Enable computer and user accounts' is set",
"18.9.108.2.1 (L2) Ensure 'Configure registry policy processing' is set"
]
for control_id in control_ids:
var_name = sanitize_variable_name(control_id)
print(f"{control_id}\n → {var_name}\n")
# Output:
# 1.1.1 (L1) Ensure 'Enforce password history' is set to '7 password(s)'
# → inspec_1_1_1_L1_Ensure_Enforce_password_history_is_set_to_7_password_s
#
# 2.2.27 (L1) Ensure 'Enable computer and user accounts' is set
# → inspec_2_2_27_L1_Ensure_Enable_computer_and_user_accounts_is_setThe converter automatically detects and converts custom InSpec resources:
# InSpec custom resource (libraries/application_config.rb)
class ApplicationConfig < Inspec.resource(1)
name 'application_config'
def value(key)
config[key]
end
end
# Converted to Ansible module (plugins/modules/application_config.py)
def main():
module = AnsibleModule(argument_spec={'key': {'type': 'str'}})
# ... implementationfrom ansible_inspec import ProfileConverter
# Step 1: Initialize converter
converter = ProfileConverter(
profile_path="./profiles/cis-ubuntu",
output_dir="./collections",
namespace="security",
collection_name="cis_ubuntu"
)
# Step 2: Convert profile
collection_path = converter.convert()
# Creates: ./collections/ansible_collections/security/cis_ubuntu/
# Step 3: Collection structure
# ansible_collections/security/cis_ubuntu/
# ├── galaxy.yml # Collection metadata
# ├── README.md # Documentation
# ├── ansible.cfg # Auto-reporting config
# ├── roles/ # Converted controls
# │ ├── filesystem/
# │ ├── network/
# │ └── ...
# ├── playbooks/ # Ready-to-use playbooks
# │ └── compliance_check.yml
# ├── plugins/
# │ ├── modules/ # Custom resources
# │ └── callback/ # Reporter plugin
# │ └── compliance_reporter.py
# └── docs/ # Documentation
# Step 4: Build tarball
tarball = converter.build()
# Creates: security-cis_ubuntu-1.0.0.tar.gz
# Step 5: Install collection
# ansible-galaxy collection install security-cis_ubuntu-1.0.0.tar.gzSeverity: CRITICAL (P0)
Status: Fixed
Date: 2026-01-11
The InSpec profile converter was only converting 4 out of 358 controls (99% loss) from CIS benchmark profiles. The root cause was a regex pattern that failed to match control IDs containing single quotes.
# Problematic pattern (v0.1.5 and earlier)
CONTROL_PATTERN = re.compile(
r"control\s+['\"]([^'\"]+)['\"]\s+do(.*?)^end",
re.MULTILINE | re.DOTALL
)The character class [^'\"]+ means "match any character EXCEPT quotes", causing the pattern to stop at the first quote inside the control ID:
control "1.1.1 (L1) Ensure 'password history' is set" do
# ^------- Match stops here -------^ <-- Only captures up to first '# Fixed pattern (v0.1.6+)
CONTROL_PATTERN = re.compile(
r"control\s+(['\"])(.+?)\1\s+do(.*?)^end",
re.MULTILINE | re.DOTALL
)Uses backreference \1 to match the same quote type that opened the string:
(['\"])- Capture opening quote (group 1)(.+?)- Capture control ID with any characters (group 2)\1- Match the SAME quote that opened the string- Control ID moved from group(1) to group(2)
- Control body moved from group(2) to group(3)
- Before: 4 controls converted (1.1% success rate)
- After: 358 controls converted (100% success rate)
- Improvement: 354 additional controls (8,850% increase)
The fix includes comprehensive regression tests:
# Test 1: Control ID with single quotes
def test_parse_control_with_quotes_in_id():
control_code = '''
control "1.1.1 (L1) Ensure 'Enforce password history' is set to '7 password(s)'" do
impact 1.0
describe file('/etc/passwd') do
it { should exist }
end
end
'''
parser = InSpecControlParser(control_code)
controls = parser.parse()
assert len(controls) == 1
assert controls[0]['id'] == "1.1.1 (L1) Ensure 'Enforce password history' is set to '7 password(s)'"
# Test 2: Mixed quote types
def test_parse_control_with_double_quotes_in_single_quoted_id():
control_code = '''
control 'test-id-with-"double"-quotes' do
impact 0.5
describe file('/test') do
it { should exist }
end
end
'''
parser = InSpecControlParser(control_code)
controls = parser.parse()
assert controls[0]['id'] == 'test-id-with-"double"-quotes'
# Test 3: Multiple controls with quotes
def test_parse_multiple_controls_with_quotes():
# Tests parsing of 3 controls with various quote patterns
# Ensures all controls are captured correctlyTo verify the fix works on your profiles:
# Clone repository
git clone https://github.com/Htunn/ansible-inspec
cd ansible-inspec
# Install with latest fix
pip install -e .
# Run tests
pytest tests/test_converter.py::TestInSpecControlParser -v
# Test on real CIS benchmark profile
ansible-inspec convert path/to/cis-profile -o /tmp/test-output
# Count converted controls
grep -c "^control " path/to/cis-profile/controls/*.rb
# Should match number of tasks in converted collection- Bug Report: Bug #1 Report
- Fixed Files:
lib/ansible_inspec/converter.py(lines 169-172, 196-198) - Test Coverage:
tests/test_converter.py(lines 178-285)
from ansible_inspec import Runner
from ansible_inspec.core import ExecutionConfig
# Configure execution
config = ExecutionConfig(
profile_path="./profiles/linux-baseline",
target="local://",
reporter="json",
output_path="local-compliance.json"
)
# Run profile
runner = Runner(config)
result = runner.run()
# Check results
if result.success:
print("✓ System is compliant!")
else:
print(f"✗ {result.controls_failed} controls failed")
print(result.error)
# Generate HTML report
result.save("compliance-report.html", format="html")from ansible_inspec import Runner
from ansible_inspec.core import ExecutionConfig
# Use Ansible inventory
config = ExecutionConfig(
profile_path="./profiles/cis-benchmark",
target="inventory.yml", # Ansible inventory file
reporter="html",
output_path=".compliance-reports/multi-host.html"
)
runner = Runner(config)
result = runner.run()
print(f"Tested: {result.hosts_total} hosts")
print(f"Passed: {result.controls_passed}/{result.controls_total}")from ansible_inspec import ProfileConverter
import subprocess
# Convert InSpec profile
converter = ProfileConverter(
profile_path="./profiles/pci-dss",
namespace="compliance",
collection_name="pci_dss"
)
collection_path = converter.convert()
tarball = converter.build()
# Install collection
subprocess.run([
"ansible-galaxy", "collection", "install",
tarball, "--force"
])
# Run collection playbook
subprocess.run([
"ansible-playbook",
f"{collection_path}/playbooks/compliance_check.yml",
"-i", "inventory.yml"
])from ansible_inspec import Runner
from ansible_inspec.core import ExecutionConfig
import sys
# Configure for CI/CD
config = ExecutionConfig(
profile_path=os.getenv("PROFILE_PATH"),
target=os.getenv("TARGET_INVENTORY"),
reporter="junit",
output_path="test-results/compliance.xml"
)
runner = Runner(config)
result = runner.run()
# Exit with appropriate code for CI/CD
sys.exit(0 if result.success else 1)from ansible_inspec import Runner
from ansible_inspec.core import ExecutionConfig
from ansible_inspec.reporters import InSpecJSONReport
import json
# Run profile
config = ExecutionConfig(
profile_path="./my-profile",
target="local://"
)
runner = Runner(config)
result = runner.run()
# Parse JSON report
report_data = json.loads(result.to_json())
# Custom analysis
for profile in report_data['profiles']:
for control in profile['controls']:
if control['results'][0]['status'] == 'failed':
print(f"FAILED: {control['id']} - {control['title']}")
print(f" Impact: {control['impact']}")
print(f" Description: {control['desc']}")from ansible_inspec import SupermarketClient, ProfileConverter
# Search for profiles
client = SupermarketClient()
profiles = client.search("CIS", limit=10)
for profile in profiles:
print(f"{profile['name']}: {profile['summary']}")
# Download and convert
profile_name = "dev-sec/linux-baseline"
profile_info = client.info(profile_name)
profile_path = client.download(profile_name, output_dir="./profiles")
# Convert to Ansible collection
converter = ProfileConverter(
profile_path=profile_path,
namespace="devsec",
collection_name="linux_baseline"
)
collection_path = converter.convert()
print(f"Collection ready: {collection_path}")ANSIBLE_INSPEC_REPORTER- Default reporter formatANSIBLE_INSPEC_OUTPUT_DIR- Default output directoryANSIBLE_CONFIG- Ansible configuration fileANSIBLE_INVENTORY- Default inventory file
# ansible.cfg
[defaults]
callbacks_enabled = compliance_reporter
callback_result_dir = .compliance-reports
[callback_compliance_reporter]
output_dir = .compliance-reports
output_format = jsonfrom ansible_inspec import Runner, ProfileConverter
from ansible_inspec.core import ExecutionConfig
from ansible_inspec.exceptions import ProfileNotFoundError, ConversionError
# Execution error handling
try:
config = ExecutionConfig(
profile_path="./nonexistent-profile",
target="local://"
)
runner = Runner(config)
result = runner.run()
except ProfileNotFoundError as e:
print(f"Profile error: {e}")
except Exception as e:
print(f"Execution error: {e}")
# Conversion error handling
try:
converter = ProfileConverter(
profile_path="./invalid-profile",
namespace="test"
)
converter.convert()
except ConversionError as e:
print(f"Conversion failed: {e}")
except Exception as e:
print(f"Unexpected error: {e}")- Documentation: https://github.com/Htunn/ansible-inspec#readme
- PyPI: https://pypi.org/project/ansible-inspec/
- Docker Hub: https://hub.docker.com/r/htunnthuthu/ansible-inspec
- Issues: https://github.com/Htunn/ansible-inspec/issues
- Changelog: https://github.com/Htunn/ansible-inspec/blob/main/CHANGELOG.md
GPL-3.0-or-later. See LICENSE for details.