Full YAML schema¶
A controls YAML file has one top-level key controls: containing a list of control objects.
controls:
- <control>
- <control>
Control object¶
- control_id: <string> # required — unique stable identifier
title: <string> # required — short human-readable name
severity: high | medium | low # required — used for report coloring and sorting
<citation-block> # required — one of: cis, custom
data_sources_needed: # required — list of: setropts, irrdbu00, dcollect
- setropts
- irrdbu00
implementation: # required
<implementation-block>
remediation: <string> # required — what to do when the control fails
Citation blocks¶
Exactly one of the following is required. They are mutually exclusive.
cis: block¶
cis:
section: <string> # e.g. "1.1.1"
level: L1 | L2
benchmark: <string> # e.g. "CIS z/OS 2.5.0"
custom: block¶
custom:
benchmark: <string> # e.g. "Internal policy" or regulation name
category: <string> # section label shown in the report
reference: <string> # optional — internal policy reference
Implementation block — pandas_query engine¶
implementation:
engine: pandas_query
dataset: <dotted-path> # e.g. setropts.fieldInfo, irrdbu00.users
select_columns: [<col>, ...] # columns shown in findings rows
filter: <pandas-query-string> # passed to DataFrame.query() before assertion
assertion:
type: <assertion-type> # see below
...
filter is a pandas .query() expression string applied to the dataset before the assertion is evaluated. Column names with spaces must be backtick-quoted: `Column Name`. String comparisons are case-sensitive by default. filter may be omitted — the entire DataFrame is then passed to the assertion.
assertion is a YAML mapping. Three types are supported:
Assertion type: no_rows¶
Pass when the filtered DataFrame is empty. Each surviving row is a finding.
assertion:
type: no_rows
pass_message: "No violations found" # optional
fail_message: "N finding(s) returned" # optional; N is the row count
Both messages are optional. Default pass_message is "No violations".
Default fail_message is "<N> finding(s)" where N is the actual row count.
Example — ensure no dataset profile has UACC=ALTER:
implementation:
engine: pandas_query
dataset: irrdbu00.datasets
select_columns: [DSBD_NAME, DSBD_UACC]
filter: "DSBD_UACC == 'ALTER'"
assertion:
type: no_rows
pass_message: "No dataset profiles with UACC=ALTER"
fail_message: "Dataset profile(s) found with UACC=ALTER"
Assertion type: scalar_compare¶
Filter must return at least one row. The Value column of the first row is read,
coerced to int if possible, then compared against expected using operator.
assertion:
type: scalar_compare
operator: "<=" # one of: <= >= > < == !=
expected: 90 # int or string to compare against
pass_message: "PASSWORD INTERVAL is {value} days (≤ 90)"
fail_message: "PASSWORD INTERVAL exceeds 90 days (value: {value})"
{value} in either message is replaced with the actual value read from the row.
If the filtered DataFrame is empty (setting not found), the control returns FAIL
with detail "Setting not found in data".
Example — ensure PASSWORD INTERVAL ≤ 90 days:
implementation:
engine: pandas_query
dataset: setropts.fieldInfo
select_columns: [Setting, Value]
filter: "Setting == 'INTERVAL'"
assertion:
type: scalar_compare
operator: "<="
expected: 90
pass_message: "PASSWORD INTERVAL is {value} days (≤ 90)"
fail_message: "PASSWORD INTERVAL exceeds 90 days (value: {value})"
Supported operators:
| Operator | Meaning |
|---|---|
<= |
actual ≤ expected |
>= |
actual ≥ expected |
< |
actual < expected |
> |
actual > expected |
== |
actual == expected |
!= |
actual != expected |
Assertion type: python_result¶
Used alongside the python engine (not pandas_query). The assertion block is documentation-only — it describes what PASS and FAIL mean, but the verdict is determined entirely by the logic block.
implementation:
engine: python
dataset: irrdbu00.users
select_columns: [USBD_NAME, USBD_NOPWD]
assertion:
type: python_result
pass_message: "STC userid is PROTECTED"
fail_message: "STC userid is not PROTECTED or does not exist"
logic: |
row = df[df['USBD_NAME'].str.upper() == 'SDSF']
if row.empty:
status = 'FAIL'
detail = "STC userid SDSF not found"
else:
nopwd = row['USBD_NOPWD'].values[0]
status = 'PASS' if nopwd == 'YES' else 'FAIL'
detail = f"NOPASSWORD={nopwd}"
findings = row.to_dict('records')
The pass_message / fail_message values serve as human-readable documentation of intent. They are not used at runtime.
Implementation block — python engine¶
implementation:
engine: python
dataset: <dotted-path> # primary DataFrame, exposed as `df`
# or combined specs: "irrdbu00.users + irrdbu00.groups"
select_columns: [<col>, ...] # informational; shown in report header
assertion:
type: python_result # optional — documentation only
pass_message: "..."
fail_message: "..."
logic: |
# Python code block
# Must set: status (str)
# May set: detail (str), findings (list of dicts)
Combined dataset specs¶
When a control needs more than one DataFrame, join them with +:
dataset: "irrdbu00.userOMVS + irrdbu00.users"
Each component is exposed under a fixed variable name:
Component in dataset |
Variable in logic |
|---|---|
irrdbu00.users |
users_df |
irrdbu00.userOMVS |
omvs_df |
irrdbu00.groups |
groups_df |
irrdbu00.groupOMVS |
gomvs_df |
irrdbu00.generalAccess |
access_df |
| any other spec | df |
Single-spec dataset values are always exposed as df.
Variables injected into logic¶
These are available regardless of which dataset is specified:
| Name | Type | Value |
|---|---|---|
df |
pd.DataFrame |
The primary DataFrame (single dataset spec) |
users_df |
pd.DataFrame |
irrdbu00.users (combined spec or always available via irrdbu00.users) |
omvs_df |
pd.DataFrame |
irrdbu00.userOMVS (combined spec) |
groups_df |
pd.DataFrame |
irrdbu00.groups (combined spec) |
gomvs_df |
pd.DataFrame |
irrdbu00.groupOMVS (combined spec) |
access_df |
pd.DataFrame |
irrdbu00.generalAccess (combined spec) |
setropts |
SETROPTS |
Full SETROPTS object |
irrdbu00 |
IRRDBU00 |
Full IRRDBU00 object (access any DataFrame via irrdbu00.generals etc.) |
dcollect |
DCOLLECT | None |
Full DCOLLECT object, or None if not loaded |
pd |
module | pandas |
Variables the logic block must set¶
| Name | Type | Required | Description |
|---|---|---|---|
status |
str |
yes | 'PASS', 'FAIL', 'REVIEW', or 'SKIP' |
detail |
str |
no | One-line summary shown under the verdict |
findings |
list[dict] |
no | Rows shown in the findings table |
If logic raises an exception, the control is marked ERROR and the traceback appears in the console output.
Dataset dotted-path reference¶
| Path | Object | DataFrame |
|---|---|---|
setropts.fieldInfo |
SETROPTS | Global SETROPTS key/value settings |
setropts.classInfo |
SETROPTS | Per-class activation flags |
irrdbu00.users |
IRRDBU00 | User base records (0200) |
irrdbu00.groups |
IRRDBU00 | Group base records (0100) |
irrdbu00.userOMVS |
IRRDBU00 | User OMVS segments (0270) |
irrdbu00.groupOMVS |
IRRDBU00 | Group OMVS segments (0170) |
irrdbu00.connectData |
IRRDBU00 | User-to-group connections (0205) |
irrdbu00.generals |
IRRDBU00 | General resource base records (0500) |
irrdbu00.generalSTDATA |
IRRDBU00 | General resource STDATA segments (0503) |
irrdbu00.generalAccess |
IRRDBU00 | General resource access lists (0510) |
irrdbu00.datasets |
IRRDBU00 | Dataset base records (0400) |
irrdbu00.datasetAccess |
IRRDBU00 | Dataset access lists (0410) |
dcollect.datasets |
DCOLLECT | Dataset catalog entries |
Complete example¶
controls:
- control_id: CIS-1.1.1
title: "Ensure PASSWORD(INTERVAL) is set to no longer than 90 days"
severity: medium
cis:
section: "1.1.1"
level: L1
benchmark: "CIS z/OS RACF Benchmark"
data_sources_needed: [setropts]
implementation:
engine: pandas_query
dataset: setropts.fieldInfo
select_columns: [Setting, Value]
filter: "Setting == 'INTERVAL'"
assertion:
type: scalar_compare
operator: "<="
expected: 90
pass_message: "PASSWORD INTERVAL is {value} days (≤ 90)"
fail_message: "PASSWORD INTERVAL exceeds 90 days (value: {value})"
remediation: >
Set the password expiry interval: SETROPTS PASSWORD(INTERVAL(90))