A CVE, short for Common Vulnerabilities and Exposures, is a publicly disclosed flaw in software that attackers can exploit. Every system running the affected package is exposed until it is fixed, and the window between disclosure and exploitation keeps shrinking. Security teams face a constant stream of CVEs across operating systems, application dependencies, and container images, and triaging each one by hand does not scale. AI-speed resolution (deciding what is affected, choosing the right fix, and executing on it in minutes instead of days) is now an operational necessity.
Not every CVE is the same problem. An OS package gets a dnf update. A library baked into an application container needs a CI/CD rebuild. An unpatched high-severity CVE with no advisory still needs immediate compensating controls. A low-severity finding with no affected systems needs only a log entry. A single static workflow cannot make those decisions correctly without human review of every event.
An Ansible Automation Platform workflow is a sequence of automation steps, such as job runs, approvals, and decisions, that executes as one unit from a single trigger. Automation orchestrator extends that idea: a system that connects multiple layers of your infrastructure (inventory, job templates, external APIs, LLM endpoints) and orchestrates them together in one inspectable flow. This guide shows how to wire an Automation orchestrator workflow that makes those routing decisions autonomously. A Task agent step reads the SBOM correlation results, inspects CVE metadata, and returns a typed JSON response selecting one of four paths. A switch step reads that JSON. Human approval gates sit in front of production changes. The entire workflow, from triage and routing to execution and audit, runs in one Automation orchestrator flow from a single trigger.
Operational Impact: High on the patch and application-redeploy paths. The triage and SBOM correlation steps are read-only.
Business Value Drivers:
Technical Value Drivers:
AI outputs feed downstream steps as typed JSON, keeping routing logic out of playbooks
Traditional AAP workflow templates branch deterministically on job success or failure. That is the right tool when you know in advance which path to take. CVE remediation is not that problem. The right response depends on whether a patch exists, whether the vulnerable code is an OS package or an application dependency, and what the CVSS score says about urgency. Those are contextual judgments that change with every CVE.
Automation orchestrator adds step types that close this gap: a Task agent step calls a language model mid-workflow, reads artifacts from upstream steps, and returns structured output. A switch step evaluates expressions against that output to pick a branch. An approval step holds the workflow until a human confirms. A script step (tech preview, generally available around RH1) runs embedded Python between job template calls. Together they let an AI make the routing decision while keeping production-mutation steps behind human gates.
Ansible Automation Platform (redhat.com)
The workflow definition lives in ao/cve-routing-workflow.json in the lightwell-patch-to-production repository. Import it into Automation orchestrator and point the Task agent steps at an OpenAI-compatible endpoint. The reference configuration uses claude-sonnet-4.6 via a hosted MaaS endpoint, but any model that accepts the OpenAI schema and returns JSON works.
| Persona | Challenge | What They Gain |
|---|---|---|
Every CVE requires manual review to decide whether to dnf update, rebuild a container, or apply a firewall rule. |
A single trigger that handles the decision autonomously, then asks for sign-off only when production changes are imminent. | |
| Branching remediation logic is scattered across EDA rulebooks, workflow templates, and ad-hoc survey variables. | All routing logic and AI inference in one inspectable Automation orchestrator workflow definition. | |
| Hard to show that AI is making defensible decisions before it touches production. | Response schema enforcement on every Task agent step, so each AI decision is a typed, auditable JSON object in the workflow history. | |
| CVE triage decisions vary by analyst, and compensating controls are applied inconsistently under time pressure. | Consistent, policy-driven triage on every CVE, with compensating-control decisions scored before and after they are applied. | |
| Hard to prove CVE exposure is handled within SLA and that AI-driven changes are governed. | A complete audit trail of every AI decision, approval, remediation, and verification for every CVE in one workflow history. |
Recommended Demos:
claude-sonnet-4.6 over a hosted MaaS endpoint at aap-cve-routing.demoredhat.com. Any endpoint that accepts the OpenAI schema and returns JSON works.The AO workflow itself has no collection dependencies; it calls existing job templates that carry their own requirements. The templates require:
| Collection | Type | Purpose |
|---|---|---|
| ansible.controller | Certified | Configuration as code to create the job templates called by the workflow |
| ansible.posix | Certified | Firewall rules in the CME compensating-control path |
| containers.podman | Certified | Container test in the OS patch path |
cme.controls |
Project | cme_query, cme_score, and per-control roles used in the CME mitigate and verify steps |
| System | Required | Notes |
|---|---|---|
| Automation orchestrator | Yes | Included in AAP 2.7+. Accessible from the AAP gateway. |
| LLM inference endpoint | Yes | OpenAI-compatible. Any hosted or on-premises model works. |
| Red Hat Trusted Profile Analyzer | Yes | Keycloak-authenticated SBOM store and CVE correlation API |
| CME MCP server | Yes for CME path | Runs on port 8000 on the builder node. Required only when cme_mitigate is the route. |
| Gitea + Gitea Actions | Yes for GitOps path | Port 3000 and 8081 (pypiserver) on the builder node. Required only when gitops_app_patch is the route. |
Operational Impact: None for triage and correlation. High for the patch and GitOps paths. Medium for compensating controls.
Credential note: Store the LLM API key, Gitea token, RHTPA OIDC client secret, and the AAP Controller OAuth token in Vault or Controller credentials. The script step (tech preview) reads Gitea credentials from environment variables injected by the workflow; GITEA_PASS is intentionally empty in the reference and must be set from a credential before this runs in production.
A trigger fires (manual launch or an EDA webhook that starts the workflow) and passes CVE fields as trigger variables. A job template step runs SBOM correlation and surfaces structured artifacts. A Task agent step reads those artifacts alongside the trigger variables, reasons through CVE type and patch availability, and outputs a typed JSON routing decision. A switch step evaluates ${triage_agent.result.content.route} against four conditions and directs the workflow to the matching path.
graph LR
T["Trigger (EDA or manual)"] --> JT["Job template:<br>CVE Correlation - TPA SBOM Lookup"]
JT --> AI["Task agent:<br>CVE Triage - Route Decision"]
AI --> SW{"Switch:<br>CVE Route Decision"}
SW -- patch --> A1["Container test"] --> A2["Approval"] --> A3["Patch & verify,<br>close loop"]
SW -- cme_mitigate --> B1["Triage report"] --> B2["Approval"] --> B3["Apply controls,<br>score"]
SW -- gitops_app_patch --> C1["Task agent:<br>repo analysis"] --> C2["Create PR"] --> C3["Wait for CI"] --> C4["SBOM rescan"]
SW -- log_only --> D1["Task agent:<br>log entry"]
A1 -- "test fails" --> E["Task agent:<br>escalation"]
Five step types appear in this workflow. Step names below follow the Automation orchestrator UI; the workflow JSON uses equivalent identifiers. Each has a distinct role and a different way of passing data to the next step.
| Step type | How it runs | How downstream steps read its output |
|---|---|---|
| Job template | Launches an existing Controller job template | ${node_id.artifacts.field}, reading variables set by ansible.builtin.set_stats in the playbook |
| Task agent | Calls the configured LLM endpoint with a prompt | ${node_id.result.content.field}, reading the response schema fields |
| Switch | Evaluates conditions against upstream outputs | Routes via from_port values: case_0, case_1, case_2, case_3, or default |
| Approval | Pauses until an authorized user approves or denies | Continues on approved, halts on denial |
| Script (tech preview) | Runs embedded Python in the workflow runtime | Stdout JSON from print(json.dumps({...})) is available to downstream steps |
| Stage | Impact | Step type |
|---|---|---|
| SBOM correlation | Low | Job template |
| AI triage and routing | None | Task agent |
| Route switch | None | Switch |
| AI triage report (CME path) | None | Job template |
| CME controls approval gate | None | Approval |
| CME mitigate and verify | Medium | Job template |
| Container test (patch path) | Low | Job template |
| Patch approval gate | None | Approval |
| Patch and verify, close loop | High | Job template |
| SBOM repo analysis (GitOps path) | None | Task agent |
| GitOps PR creation | Low | Job template |
| Wait for Gitea CI | None | Script (tech preview) |
| SBOM rescan | Low | Job template |
| Log only | None | Task agent |
Operational Impact: Low
The first step is CVE Correlation - TPA SBOM Lookup, a job template step that launches the CVE Correlation job template. That template queries Trusted Profile Analyzer for SBOMs containing the affected package URL, maps SBOM source names to inventory hostnames, and calls ansible.builtin.set_stats to surface structured artifacts back to the orchestrator.
The Task agent reads three of those artifacts by name: ao_is_affected, ao_affected_count, and ao_cvss_score. The ao_high_severity artifact also feeds the prompt. For this to work, the CVE Correlation playbook must export those exact variable names with set_stats. Add these tasks to playbooks/correlate_cve.yml if they are not already present:
- name: Expose correlation facts to Automation orchestrator
ansible.builtin.set_stats:
data:
ao_is_affected: "{{ (affected_hosts | length > 0) | string }}"
ao_affected_count: "{{ affected_hosts | length }}"
ao_cvss_score: "{{ exposure_report.cvss_base_score | default(0.0) }}"
ao_high_severity: "{{ (exposure_report.cvss_base_score | default(0) | float >= 7.0) | string }}"
aggregate: false
Artifact naming convention: Prefix AO-facing stats with
ao_to distinguish them from variables used only within the playbook. Switch conditions and agent prompts reference them as${correlate_cve_job.artifacts.ao_is_affected}.
Operational Impact: None
[AI] CVE Triage - Patch Availability and Route Decision is a Task agent step. Its prompt combines trigger variables (${trigger.demo_cve_id}, ${trigger.demo_advisory_id}, ${trigger.demo_app_fixed_version}) with the correlation artifacts. The response schema enforces structure:
{
"patch_available": true,
"advisory_id": "RHSA-2026:4521",
"route": "patch",
"urgency": "CRITICAL",
"cvss_score": 10.0,
"affected_count": 3,
"reasoning": "one sentence explaining the routing decision"
}
The routing logic in the prompt is explicit and deterministic:
demo_app_fixed_version is set, the CVE is an application dependency, so always route gitops_app_patch.demo_advisory_id is non-empty, a patch exists, so route patch.cme_mitigate.log_only.Why put logic in the prompt and not in a switch?
The Task agent can read both trigger variables and upstream job artifacts in one step. A switch step cannot call an external API or reason over a combination of inputs. The agent prompt acts as the decision function; the switch reads its output.
Step configuration:
| Parameter | Value |
|---|---|
tool_selection_strategy |
NONE: this agent reasons and returns JSON, it does not call tools |
response_schema |
Seven properties: route, urgency, reasoning, cvss_score, advisory_id, affected_count, patch_available |
| LLM | claude-sonnet-4.6 via MaaS endpoint |
Operational Impact: None
CVE Route Decision is a switch step with four cases. Each condition reads ${triage_agent.result.content.route} directly from the Task agent’s response schema:
{ "condition": "${triage_agent.result.content.route} == \"patch\"" }
{ "condition": "${triage_agent.result.content.route} == \"cme_mitigate\"" }
{ "condition": "${triage_agent.result.content.route} == \"gitops_app_patch\"" }
{ "condition": "${triage_agent.result.content.route} == \"log_only\"" }
The default_port catches any response schema violation and stops the workflow without executing any remediation.
Operational Impact: High
Container Test - Validate Patch Before Production runs the Container Test job template. It passes two trigger variables as extra_vars: demo_cve_fixed_version and demo_cve_vulnerable_version. The job template builds a UBI container with the vulnerable RPM, applies dnf update-minimal --advisory, and exports the result as ao_safe_to_deploy.
A second switch reads ${container_test_job.artifacts.ao_safe_to_deploy} == "true". On case_0, execution proceeds to the Approve Production Patch approval step. On default, the workflow routes to [AI] Container Test Failed - Escalate:
{
"escalation_summary": "string",
"recommended_actions": "string",
"severity": "HIGH"
}
This agent receives the pre- and post-patch version artifacts so its escalation message includes the exact package state that failed.
The Approve Production Patch approval step pauses until an authorized user confirms. Only then does Patch and Verify - Production run. After a successful patch, Close Loop - Upload SBOMs, Remove CME Controls, Final Report refreshes RHTPA and removes any temporary compensating controls.
RBAC: Scope the approval role.
Grant the approval permission on this workflow only to the team that owns the production change window. Engineers who investigate the escalation agent output should not also be able to approve their own escalation bypass.
Container test stats the playbook must export:
- name: Expose container test result to Automation orchestrator
ansible.builtin.set_stats:
data:
ao_safe_to_deploy: "{{ (test_passed | bool) | string }}"
ao_test_result: "{{ 'passed' if test_passed | bool else 'failed' }}"
ao_pre_patch_version: "{{ pre_patch_version.stdout | default('unknown') }}"
ao_post_patch_version: "{{ post_patch_version.stdout | default('unknown') }}"
aggregate: false
Operational Impact: Medium
When no patch exists, the AI route is cme_mitigate. The first step in this path is Create AI Triage Report, which runs the AI CVE Triage job template. This is a read-only enrichment step: it passes the CVE fields as extra_vars and writes an HTML triage report to the report server.
After the report, Approve CME Compensating Controls holds the workflow. An operator reviews the triage report, then approves. CME Mitigate - Apply Compensating Controls runs, followed by CME Verify - CVSS Attenuation Before vs After. The verify step scores the effective CVSS before and after controls.
RBAC: Approve CME controls separately from production patches.
CME controls change host posture. Assign the approve permission on
approval_cmeto a different role thanapproval_prodif your change advisory process distinguishes temporary compensating controls from patching.
Operational Impact: Low to Medium
For application-dependency CVEs, the route is gitops_app_patch. The first step is a second Task agent step: [AI] SBOM Correlation - Which Repos Need the Fix?. It reads the SBOM correlation artifacts from correlate_cve_job and the trigger variables, then returns a typed decision:
{
"repo_name": "config-service",
"repo_owner": "demo-admin",
"dependency_file": "requirements.txt",
"affected_library": "pyyaml",
"current_version": "6.0.1",
"fixed_version": "6.0.2",
"affected_systems_count": 3,
"pr_title": "fix(security): CVE-2026-52891 - update pyyaml to 6.0.2",
"reasoning": "string"
}
Every field from this response is passed directly to App GitOps - Create PR as extra_vars using the ${sbom_repo_analysis_agent.result.content.*} syntax:
{
"demo_app_name": "${sbom_repo_analysis_agent.result.content.repo_name}",
"demo_gitea_owner": "${sbom_repo_analysis_agent.result.content.repo_owner}",
"demo_app_fixed_version": "${sbom_repo_analysis_agent.result.content.fixed_version}",
"demo_app_dependency_file": "${sbom_repo_analysis_agent.result.content.dependency_file}",
"demo_app_affected_library": "${sbom_repo_analysis_agent.result.content.affected_library}"
}
The GitOps job template opens a PR and exports ao_pr_number via set_stats. The next step is [Script] Wait for PR Merge and Gitea CI Pipeline, a script step (tech preview) that runs embedded Python. It polls the Gitea API for the PR merge and then polls the Actions run linked to the merge SHA:
for _ in range(60):
pr = get(f'{gitea_base}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}')
if pr.get('merged'):
merge_sha = pr.get('merge_commit_sha', '')
break
time.sleep(30)
The script raises an exception, which fails the workflow, if the PR is closed without merging or if CI does not complete within 30 minutes (60 polls at 30-second intervals). On success, SBOM Scan and Upload - Confirm CVE Resolved in TPA rescans the fleet and uploads fresh SBOMs to verify the library is gone.
Credential handling for the script step.
The
GITEA_PASSenvironment variable is empty in the reference definition. Before running in production, inject the Gitea password or token via a Controller credential mapped to theGITEA_PASSenvironment variable. Do not commit credentials to the workflow JSON.
Operational Impact: None
When the route is log_only, [AI] Log Low Severity CVE - No Immediate Action fires. It receives the triage agent’s output and writes a structured log entry with a 30-day review recommendation:
{
"log_entry": "string",
"recommended_monitoring": "string",
"review_in_days": 30
}
No hosts are changed. The log entry becomes part of the workflow job history in Automation orchestrator.
From Automation orchestrator, launch the workflow with these input variables to drive the patch route (OS CVE with an advisory):
{
"demo_cve_id": "CVE-2026-51234",
"demo_cve_package": "python-cryptography",
"demo_cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
"demo_advisory_id": "RHSA-2026:4521",
"demo_cve_fixed_version": "43.0.1-2.el9",
"demo_cve_vulnerable_version": "36.0.1-5.el9_6"
}
The triage agent returns route: "patch". The switch activates case_0. The container test job runs and exports ao_safe_to_deploy: "true". The workflow pauses at Approve Production Patch. After approval, the fleet is patched and the close-loop job uploads updated SBOMs.
To drive the GitOps route, supply demo_app_cve_id and demo_app_fixed_version and omit demo_advisory_id. The triage agent returns route: "gitops_app_patch" regardless of CVSS.
To drive the CME path, omit demo_advisory_id and demo_app_fixed_version with a CVSS >= 7.0 vector. To drive log only, supply a CVSS below 7.0.
| Symptom | Likely Cause | Fix |
|---|---|---|
Triage agent returns an unexpected route |
demo_advisory_id or demo_app_fixed_version are empty strings, which the prompt treats as unset |
Pass null or omit these fields entirely in the trigger; an empty string "" is not the same as absent for the agent’s routing logic |
| Triage step fails before returning a route | LLM endpoint is unreachable, returns an error, or the API key is invalid | Confirm the endpoint URL and API key; add a retry on the Task agent step, or treat workflow failure as the trigger for human triage |
Switch hits default port and halts |
The Task agent returned a response outside the four valid route values, or response_schema was not enforced |
Confirm the LLM endpoint returns valid JSON matching the schema; enable schema validation on the Task agent step |
Container test switch hits default |
ao_safe_to_deploy was not exported, or was exported as False (capital F) |
Verify set_stats is the last task in the container test playbook and exports the value as the string "true" or "false" |
Script step raises PR_NUMBER not set |
The GitOps job template’s App GitOps Remediate playbook did not call set_stats with ao_pr_number |
Add set_stats to playbooks/app_gitops_remediate.yml with ao_pr_number: "{{ pr_number }}" |
| Script step times out waiting for CI | Gitea act runner is not registered, or the runner is busy | Run setup/deploy_gitea.yml to confirm the runner, and check port 3000 is accessible from the AO runtime |
| Approval step never receives a notification | The notifier is not configured on the approval step | In Automation orchestrator, set a notification method (email or webhook) on each approval step so approvers know to act |
| Maturity | Description |
|---|---|
| Import the workflow and run the manual trigger with hardcoded test variables. Verify all four routes reach their terminal step. Disable approval steps and use survey-based overrides during initial validation. | |
| Connect EDA so a webhook starts the workflow automatically. Enable both approval steps. Add notification to approval steps so approvers are alerted by email or webhook. Scope the approval RBAC roles. | |
| Replace the demo triage stand-in with your production LLM endpoint and add retrieval context (CVE feeds, internal risk register). Wire the escalation agent output to a ServiceNow or Jira ticket. Auto-approve the CME path when CVSS attenuation meets a policy threshold. Consider the log-only path as input to scheduled patch campaigns. |
A single Automation orchestrator workflow handles the full CVE lifecycle. The SBOM correlation job populates artifacts that feed an AI triage Task agent. That agent returns a typed JSON decision selecting one of four remediation paths. A switch step reads the route field, no playbook logic required. Human approval steps protect the patch and CME paths from autonomous production mutation. A Python script step (tech preview) bridges the gap between PR creation and CI completion on the GitOps path. Every step, from AI decision and approval to job run and script result, appears in the Automation orchestrator job history as an auditable record.
cme_mitigate path