oscap attestor
OpenSCAP is the Red Hat-maintained reference implementation of the SCAP (Security Content Automation Protocol) stack — it evaluates a host against XCCDF benchmarks (CIS, DoD STIG, PCI-DSS, ANSSI, CUI, etc.) using OVAL probes and writes an XCCDF results XML report. Under cilock, every oscap xccdf eval run becomes a signed oscap/v0.1 attestation carrying the parsed pass/fail/notapplicable counts, the failed-rule list, and a digest of the raw XCCDF XML alongside the file itself in product/v0.3.
| Name | oscap |
|---|---|
| Predicate type | https://aflock.ai/attestations/oscap/v0.1 |
| Lifecycle | postproduct |
| Default binary? | No |
| Category | compliance-scan (primary) |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when |
|
The facts in this box are generated from the cilock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.
Validated invocation
cilock run --step oscap-scan \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations oscap,environment,git \
-- oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_standard \
--results oscap-results.xml \
/usr/share/xml/scap/ssg/content/ssg-amzn2023-ds.xml
This is the 29-oscap example verbatim, validated end-to-end against a fresh Amazon Linux 2023 EC2 host using the SCAP Security Guide (SSG) standard profile from ssg-amzn2023-ds.xml. The scan produced 11 pass, 3 fail, 64 N/A — real findings, not synthetic fixtures. The three failed rules (rpm_verify_permissions, file_permissions_library_dirs, grub2_nousb_argument) reflect actual gaps on the host.
A few things to know about this invocation:
- No soft-fail flag needed.
oscap xccdf evalexits 0 when the scan completes, regardless of how many rules failed — the rule outcomes live inside the XCCDF results XML.command-run/v0.1records exit 0 even whenfailCount > 0. The gate belongs atcilock verifytime over the parsedoscap/v0.1predicate, not at scan time. --results <file>is required. Without itoscaponly prints to stdout and there is no XML file for cilock'sproduct/v0.3andoscapattestors to ingest.- Datastream path is distribution-specific. AL2023 ships SSG content at
/usr/share/xml/scap/ssg/content/ssg-amzn2023-ds.xml; RHEL 9 usesssg-rhel9-ds.xml, Ubuntu 22.04 usesssg-ubuntu2204-ds.xml, etc. Thescap-security-guidepackage (RHEL/Fedora) orssg-base(Debian/Ubuntu) installs them.
What gets captured
Each cilock run emits an in-toto envelope whose predicate carries the following attestor types:
| Attestor type | Captures |
|---|---|
https://aflock.ai/attestations/command-run/v0.1 | Real oscap xccdf eval ... argv, env, exit code, stdout/stderr |
https://aflock.ai/attestations/material/v0.3 | Merkle tree of inputs (the SSG datastream XML is read, not in cwd) |
https://aflock.ai/attestations/product/v0.3 | Merkle tree of outputs, including oscap-results.xml |
https://aflock.ai/attestations/oscap/v0.1 | Parsed XCCDF: profile, benchmark, target host, per-result counts, failed-rule list, reportDigestSet.sha256 |
https://aflock.ai/attestations/environment/v0.1 | OS, arch, user, env vars (PII-filtered) |
https://aflock.ai/attestations/git/v0.1 | Commit SHA, branch, remotes |
The oscap/v0.1 predicate's reportDigestSet.sha256 exactly matches the digest of the oscap-results.xml leaf in the product/v0.3 tree. That is the chain that makes the findings verifiable — you can't swap in a different XCCDF report without invalidating the product tree. The oscap attestor also publishes three SHA-256 subjects on the envelope: profile:<profile-id>, host:<target-hostname>, and benchmark:<benchmark-id>, so a policy can pin "this attestation is the STIG scan for this host."
Why this shape
| Antipattern | This page |
|---|---|
cilock run ... -- bash -c "cp scan.xml results.xml" | cilock run ... -- oscap xccdf eval --results oscap-results.xml ... |
command-run records bash -c "cp ..." — useless | command-run records the real oscap xccdf eval argv |
| Product attestor digests a copy of a file written outside cilock | Product attestor digests the XCCDF XML oscap actually wrote |
| Tool execution happens outside cilock's syscall view | oscap runs as cilock's direct child; the spy traces its syscalls |
oscap predicate parses a file of unknown provenance | oscap predicate parses the same file cilock just hashed into product |
cilock invokes oscap directly — no bash -c wrapper, no cp of a pre-existing report, no echo scan-done placeholder. This page was promoted from the 29-oscap validated example precisely because earlier internal docs used those antipatterns; the antipattern sweep cleaned the attestor reference page, and this user-facing page inherits the corrected shape. The three guarantees — real argv in command-run, ptrace coverage of the scan, product digest matching the parsed report — only hold when cilock is the tool's direct parent.
Validate it locally
# Generate a signing key (one-time).
openssl genpkey -algorithm ed25519 -out key.pem
# Run cilock + oscap against the host.
cilock run --step oscap-scan \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations oscap,environment,git \
-- oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_standard \
--results oscap-results.xml \
/usr/share/xml/scap/ssg/content/ssg-amzn2023-ds.xml
# Confirm the predicate carries the expected attestor types.
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations | map(.type)'
Expected output:
[
"https://aflock.ai/attestations/environment/v0.1",
"https://aflock.ai/attestations/git/v0.1",
"https://aflock.ai/attestations/material/v0.3",
"https://aflock.ai/attestations/command-run/v0.1",
"https://aflock.ai/attestations/product/v0.3",
"https://aflock.ai/attestations/oscap/v0.1"
]
Confirm oscap's real argv ended up in command-run:
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[]
| select(.type=="https://aflock.ai/attestations/command-run/v0.1")
| .attestation.cmd'
Expected output (no bash, no cp — the literal oscap argv):
[
"oscap",
"xccdf",
"eval",
"--profile",
"xccdf_org.ssgproject.content_profile_standard",
"--results",
"oscap-results.xml",
"/usr/share/xml/scap/ssg/content/ssg-amzn2023-ds.xml"
]
Extract the pass/fail/N/A counts and the failed-rule list from the oscap/v0.1 predicate:
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[]
| select(.type=="https://aflock.ai/attestations/oscap/v0.1")
| .attestation.scanSummary
| {profile, benchmarkId, targetSystem,
passCount, failCount, notApplicableCount, errorCount,
failedRules: [.failedRules[] | {idref, severity}]}'
Against the 29-oscap AL2023 host you should see passCount: 11, failCount: 3, notApplicableCount: 64 and the three failed rules listed above.
How a verifier consumes this
The oscap attestor is a postproduct lifecycle attestor with predicate type https://aflock.ai/attestations/oscap/v0.1. It does not shell out to oscap; it reads a pre-existing XCCDF/ARF XML report from the step's products. It populates three top-level fields:
reportFile— path of the XCCDF/ARF XML file picked up from products.reportDigestSet— digest set of that file, integrity-checked against the product entry before parsing.scanSummary— structured roll-up of the first`<TestResult>`in the document:profile— theidreffrom the`<profile>`child of`<TestResult>`.benchmarkId— theidattribute of the XCCDF`<Benchmark>`element.targetSystem— the trimmed contents of the`<target>`element.passCount,failCount,notApplicableCount,errorCount— aggregate counts derived from`<rule-result>`outcomes.failedRules[]—{ idref, severity, result }for every rule whose`<result>`isfail.
Subjects published (each value hashed with SHA-256):
profile:<profile-id>— the XCCDF profile that was evaluated.host:<target-hostname>— the system that was scanned.benchmark:<benchmark-id>— the XCCDF benchmark document.
Pair this attestor with policyverify to gate promotion on failCount == 0.
Output shape
{
"reportFile": "results/oscap-results.xml",
"reportDigestSet": { "sha256": "..." },
"scanSummary": {
"profile": "xccdf_org.ssgproject.content_profile_stig",
"benchmarkId": "xccdf_org.ssgproject.content_benchmark_RHEL-9",
"targetSystem": "host.example.com",
"passCount": 312,
"failCount": 4,
"notApplicableCount": 27,
"errorCount": 0,
"failedRules": [
{
"idref": "xccdf_org.ssgproject.content_rule_package_telnet_removed",
"severity": "high",
"result": "fail"
}
]
}
}
Notes
- Profile selection. SSG ships a dozen profiles per OS — the canonical ones are
xccdf_org.ssgproject.content_profile_standard(baseline),xccdf_org.ssgproject.content_profile_cis/_cis_server_l1/_cis_workstation_l1(CIS),xccdf_org.ssgproject.content_profile_stig(DISA STIG),xccdf_org.ssgproject.content_profile_pci-dss(PCI),xccdf_org.ssgproject.content_profile_cui(NIST 800-171), andxccdf_org.ssgproject.content_profile_anssi_bp28_high.oscap info <datastream>lists every profile id available in a given DS. - Datastream paths by distro. RHEL/CentOS/Rocky/Alma:
/usr/share/xml/scap/ssg/content/ssg-rhel{8,9,10}-ds.xml. Fedora:ssg-fedora-ds.xml. AL2023:ssg-amzn2023-ds.xml. Ubuntu:ssg-ubuntu{2004,2204,2404}-ds.xml. Install viadnf install scap-security-guide(RHEL/Fedora) orapt install ssg-base ssg-debderived(Debian/Ubuntu). - SCAP spec triad. SCAP is an umbrella for several XML grammars: XCCDF (the human-authored benchmark with rule descriptions, profiles, and remediation text), OVAL (the machine-checkable probes XCCDF rules delegate to), and CPE (the platform identifiers that gate which rules apply).
oscap xccdf evalwalks all three and writes a single XCCDF results XML — that file is what cilock attests. - ARF wrappers.
oscapcan also emit an Asset Reporting Format (--results-arf arf.xml) that wraps XCCDF in a richer envelope. The rookeryoscapattestor handles both: it walks XML tokens looking for the first`<Benchmark>`element regardless of nesting depth. - Result strings. The attestor buckets rule outcomes by exact match on
pass,fail,notapplicable(andnot applicable), anderror. Other XCCDF result values —notchecked,informational,fixed,unknown— are present in the raw XML but not counted in the predicate's roll-up.
Gotchas
- Does not shell out to
oscap. The attestor reads a pre-existing XCCDF/ARF XML report from products; you must runoscap xccdf eval --results <path>(or equivalent) in a prior step and surface the file as a product. - File-suffix filter. Only products whose path ends in
.xmlare considered; reports written with other extensions are skipped. - XCCDF namespace check. A candidate file must contain the literal string
http://checklists.nist.gov/xccdf/1.2as a fast pre-filter before structural parse; non-XCCDF XML is rejected. - Digest integrity. The file is re-hashed and compared against the product digest before parsing; a mismatch causes that candidate to be skipped.
- First match wins. Iteration order over products is non-deterministic; if multiple files look like XCCDF reports, only the first accepted one is attested.
- ARF wrappers. The decoder walks tokens looking for the first
`<Benchmark>`element regardless of depth, so XCCDF wrapped in an ARF`<asset-report-collection>`envelope is supported. - First TestResult only. Only
bm.Results[0]is summarized —oscap xccdf evalproduces exactly one, but documents containing multiple are otherwise ignored. - Result strings. Counts are bucketed by exact match on
pass,fail,notapplicable/not applicable, anderror; other XCCDF result values (notchecked,informational,fixed,unknown) are recorded by neither the counters norfailedRules. - Only failing rules are listed.
failedRulescontains entries where`<result>`isfail; non-pass-but-non-fail outcomes (e.g.,error) are counted but not enumerated.
FAQ
Does cilock support OpenSCAP?
Yes. Cilock invokes the upstream oscap binary unchanged and captures its XCCDF results XML via the native oscap attestor. No OpenSCAP fork, no patched build — oscap xccdf eval runs exactly as it would outside cilock, and the resulting XML is parsed into a signed oscap/v0.1 predicate.
Which SCAP profiles can I scan under cilock?
Any profile shipped in your SCAP Security Guide datastream — CIS Level 1 / Level 2, DISA STIG, PCI-DSS, NIST 800-171 (CUI), ANSSI BP-028, the SSG standard baseline, and any custom profile you author. Cilock is profile-agnostic: it captures whatever XCCDF results XML oscap writes, and the oscap/v0.1 predicate records the profile id (xccdf_org.ssgproject.content_profile_*) alongside the per-rule outcomes.
Can I use the SCAP results in a GRC platform?
Yes. The raw XCCDF results XML lands in product/v0.3 as a Merkle leaf, so it's preserved verbatim — feed it directly into Tenable, Wazuh, Splunk Phantom, ServiceNow GRC, or any tool that already speaks XCCDF/ARF. The signed oscap/v0.1 predicate gives you a tamper-evident roll-up (counts plus failed-rule list) that's easier to evaluate in Rego or OPA, and the envelope subjects (profile:, host:, benchmark:) let a verifier confirm the scan was the right benchmark on the right machine before trusting the counts.
Does oscap exit non-zero on findings?
No — unlike SARIF tools like Checkov or gosec, oscap xccdf eval exits 0 whenever the scan completes successfully, even if many rules failed. The rule outcomes are inside the XCCDF XML, not in the process exit code. That means cilock's command-run/v0.1 records exit 0 and no soft-fail flag is needed. Enforce thresholds in policy (e.g., failCount == 0 in Rego against the oscap/v0.1 predicate at cilock verify time).
See also
29-oscapvalidated example — the AL2023 + SSGstandardreference run this page is built from- SCAP Security Guide (ComplianceAsCode) — the upstream source of every
ssg-*-ds.xmldatastream - OpenSCAP project — the
oscapCLI, libraries, and SCAP toolchain inspec- Tools index