From 77c1819d528df24e172dc1437535a0806bac84d8 Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Mon, 7 Sep 2026 13:17:03 +0200 Subject: [PATCH 1/6] Add disa-stig-quarterly-update Claude skill. Guides assessing, implementing, and describing DISA STIG quarterly benchmark updates, with a diff-report generator and cheap test fixtures to exercise the pipeline without a real release. --- .../disa-stig-quarterly-update/SKILL.md | 114 ++++++++++++++++++ .../reference/01-assess.md | 96 +++++++++++++++ .../reference/02-classify-diffs.md | 97 +++++++++++++++ .../reference/03-implement.md | 92 ++++++++++++++ .../reference/04-pr-description.md | 76 ++++++++++++ .../reference/architectural-facts.md | 79 ++++++++++++ .../reference/observations.md | 66 ++++++++++ .../reference/xccdf-format.md | 78 ++++++++++++ .../scripts/build_diff_report.py | 77 ++++++++++++ .../test-fixtures/README.md | 47 ++++++++ .../compare_ds_diffs_sample/RHEL-09-211010 | 33 +++++ .../disa-stig-rhel9-test-v1-xccdf-manual.xml | 7 ++ .../disa-stig-rhel9-test-v2-xccdf-manual.xml | 7 ++ 13 files changed, 869 insertions(+) create mode 100644 .claude/skills/disa-stig-quarterly-update/SKILL.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/01-assess.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/02-classify-diffs.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/03-implement.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/04-pr-description.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/architectural-facts.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/observations.md create mode 100644 .claude/skills/disa-stig-quarterly-update/reference/xccdf-format.md create mode 100644 .claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py create mode 100644 .claude/skills/disa-stig-quarterly-update/test-fixtures/README.md create mode 100644 .claude/skills/disa-stig-quarterly-update/test-fixtures/compare_ds_diffs_sample/RHEL-09-211010 create mode 100644 .claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v1-xccdf-manual.xml create mode 100644 .claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v2-xccdf-manual.xml diff --git a/.claude/skills/disa-stig-quarterly-update/SKILL.md b/.claude/skills/disa-stig-quarterly-update/SKILL.md new file mode 100644 index 000000000000..e1621084d818 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/SKILL.md @@ -0,0 +1,114 @@ +--- +name: disa-stig-quarterly-update +description: Assess, implement, and describe a DISA STIG quarterly benchmark update for a product in this repo. Use when a new DISA STIG release drops for a product already covered here, when diffing two xccdf-manual.xml versions with compare_ds.py, when classifying STIG diff changes (prose vs OVAL vs new rule vs removal), or when writing a DISA STIG update PR description. +--- + +# DISA STIG quarterly update + +Three phases: **assess** the new release, **implement** the changes, **describe** them in the +PR. Each phase has its own reference doc; read only the one you're on. + +- `reference/01-assess.md` - download, diff with `compare_ds.py`, build the diff report +- `reference/02-classify-diffs.md` - prose vs OVAL vs new-rule vs removal vs control-file +- `reference/03-implement.md` - commits, branches, PR, shared-file cross-PR pattern +- `reference/04-pr-description.md` - PR description template and style +- `reference/xccdf-format.md` - XCCDF XML structure, DISA-field-to-CaC mapping +- `reference/architectural-facts.md` - `audit_watches_style`, control-file variables, + `policy/stig` overrides, `file_permissions` thresholds, one-reference-file-per-product build + constraint +- `reference/observations.md` - daemons resetting file modes on reboot, SELinux inotify denials, + Contest ansible/bash asymmetry, container guards + +## Hard rules + +- **Diffs are copied verbatim, never from memory.** Every diff embedded in a report, analysis, + or comment must be the exact text `compare_ds.py` produced. If a diff looks wrong, re-run + `compare_ds.py`; don't hand-patch or reconstruct it from a prior read. +- **OVAL and Ansible/Bash remediations are the priority; OCIL is not.** Red Hat does not update + OCIL entries. +- **No internal issue IDs in this repo.** Never write an internal tracker ID into a commit, PR, + or comment here - this is a public upstream repository. +- **Symbolic file modes, not octal**, in Ansible `mode:` and Bash `chmod` (`u=rw,g=r,o=r`, not + `0644`). +- **STIG ID -> rule mapping lives only in the control file** for RHEL-family products + (`products//controls/*.yml`). Never add `stigid@:` to `rule.yml` there - + that's for Oracle Linux and SLE. +- **One `*-xccdf-manual.xml` per product** in `shared/references/`. Swap it with `git rm` + + `git add` as the first commit, not two files coexisting. +- **No pending-work sections in PR descriptions.** Describe only what the PR implements. + +## The diff report replaces spreadsheet tracking and file-server hosting + +The old process for this copy-pasted changed STIG IDs into a shared spreadsheet and `scp`'d +`diff2html` output to an internal file server, linking back to it from the spreadsheet. Neither +belongs to a change that lives entirely in this repo - a spreadsheet needs someone to have an +account and keep tabs and links in sync by hand, and a file-server upload is invisible to anyone +reviewing the PR itself. + +`scripts/build_diff_report.py` replaces both: it turns raw `compare_ds.py --disa-content +--rule-diffs` output into one markdown file per product, with every raw diff embedded verbatim +in a collapsible section, ready to read (and review) inside the PR: + +```bash +python3 .claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py \ + \ + --product rhel9 --from-version v2r8 --to-version v2r9 +``` + +Output shape (one section per STIG ID): + +```markdown +## RHEL-09-255120 + +CaC rule: `TODO` +Classification: `TODO` + +
+Diff: RHEL-09-255120 + +```diff +--- RHEL-09-255120 ++++ RHEL-09-255120 +... +``` + +
+ +Action: TODO +``` + +GitHub and GitLab both render fenced ` ```diff ` blocks with the same red/green coloring +`diff2html` produced - nothing is lost by dropping the HTML step. Fill in `CaC rule:`, +`Classification:` (`reference/02-classify-diffs.md`), and `Action:` by hand once you've read +each diff; never edit the text inside the fence. Commit the report to the feature branch so it +travels with the PR - git history is the tracking record, there's no sheet tab or external link +to keep in sync. + +## Delegate to neighboring skills, don't duplicate them + +This skill owns the DISA-specific parts: obtaining and diffing STIG releases, classifying the +changes, and writing the STIG-specific parts of the PR description. For everything else in the +implementation phase, use the skill that already owns it: + +- **`find-rule`** / **`map-requirement`** / **`map-controls`** - check whether an existing rule + already covers a STIG ID before treating it as a new rule. +- **`create-rule`** - once a STIG ID is confirmed to need a genuinely new rule. +- **`resolve-rule-variables`** - look up which XCCDF variable a rule exposes and pick a value key, + instead of hardcoding a changed value. +- **`create-test-scenarios`** / **`test-rule`** / **`run-tests`** - test coverage and validation + for a changed or new rule. +- **`build-product`** - rebuild the product's data stream to sanity-check an OVAL or template + change. +- **`draft-pr`** - push the branch and open the PR with a prefilled title/labels/milestone. Its + generic Description/Rationale/Review-Hints body should be replaced with the STIG-specific + content from `reference/04-pr-description.md`. + +## Testing this skill cheaply + +Don't spend a full quarterly release cycle to sanity-check the pipeline. `test-fixtures/` has a +pair of one-rule benchmark files - built from a real rule lifted out of +`shared/references/disa-stig-rhel9-v2r9-xccdf-manual.xml` - that differ by exactly one character +(a hyphen removed from one rule's title), plus the real, regeneratable `compare_ds.py` output for +that pair. This exercises the full assess pipeline, including `build_diff_report.py`'s output +shape and the `no-action` (punctuation-only) classification path, in under a second and without +touching a real release. See `test-fixtures/README.md`. diff --git a/.claude/skills/disa-stig-quarterly-update/reference/01-assess.md b/.claude/skills/disa-stig-quarterly-update/reference/01-assess.md new file mode 100644 index 000000000000..5910ef827aa6 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/01-assess.md @@ -0,0 +1,96 @@ +# Phase 1: assess a new DISA STIG release + +## 1. Note the current and target versions + +Check the current version in `products//profiles/stig.profile` (`metadata.version`) +against the latest release on https://www.cyber.mil/stigs/downloads. Do this for every product +in scope for the update (e.g. rhel8, rhel9, rhel10 are updated together when their release +windows overlap). + +## 2. Obtain the new STIG files + +Download from https://www.cyber.mil/stigs/downloads. Use the `*-xccdf-manual.xml` file, not +`*-xccdf-scap.xml`: + +- `*-xccdf-manual.xml` - complete requirements with human-readable procedures. Source of truth. +- `*-xccdf-scap.xml` - automated subset DISA ships with OVAL checks, not always published. + Used later for the Contest `disa-alignment` test, not for diffing. + +## 3. Diff the previous and new manual XML with `compare_ds.py` + +```bash +mkdir -p /tmp/--to--diffs +python3 utils/compare_ds.py \ + --disa-content --rule-diffs \ + --output-dir /tmp/--to--diffs \ + /-xccdf-manual.xml \ + shared/references/-xccdf-manual.xml \ + > /tmp/--to--stdout.txt 2>&1 +``` + +Keep the stdout capture - it lists rules that were added or removed outright (`"X was added in +new data stream."` / `"X is missing in new data stream."`), which the per-rule diff files don't +summarize on their own. + +This produces one unified diff file per changed STIG ID, in the `[fieldname]: value` format +described in `reference/xccdf-format.md`. A STIG ID with no behavioral or prose change produces +no diff file at all - `compare_ds.py` only emits a file when something changed. + +## 4. Build the diff report (no spreadsheet, no file-server upload) + +The previous version of this process copy-pasted changed STIG IDs into a shared spreadsheet +tab and `scp`'d `diff2html` output to an internal file server, linking back to it from the +spreadsheet. Both are external dependencies that don't belong to a change that lives entirely +in this git repo. Replace both with one markdown file, generated straight from the diffs: + +```bash +python3 .claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py \ + /tmp/--to--diffs \ + --to--diff-report.md \ + --product --from-version --to-version +``` + +This writes one `## STIG-ID` section per changed rule, each with a `CaC rule:` / +`Classification:` placeholder and the raw diff embedded verbatim inside a collapsible +`
` block (see the skill's `SKILL.md` for the exact shape). GitHub and GitLab both +render fenced ` ```diff ` blocks with the same red/green coloring `diff2html` produced, so the +report is just as readable while being a single file that lives and travels with the PR. + +Fill in `CaC rule:`, `Classification:` (see `reference/02-classify-diffs.md`), and `Action:` by +reading each embedded diff. Never edit the diff text itself - if a diff looks wrong, re-run +`compare_ds.py` and regenerate the report, don't hand-patch the fenced block. + +Commit the report to the feature branch (repo root or wherever the team keeps working notes) so +reviewers can see the full reasoning in the PR diff. There is no sheet tab to duplicate and no +external link to keep in sync - git history is the record. + +## 5. Update the reference XML files (once assessment is approved) + +```bash +git rm shared/references/-xccdf-manual.xml +git add shared/references/-xccdf-manual.xml +``` + +Do this as the first commit on the branch - see `reference/03-implement.md`. If DISA published a +new `*-xccdf-scap.xml`, swap that too; if not, the Contest `disa-alignment` test simply won't run +for this release, which is expected. + +## How to assess each changed rule + +For every STIG ID in the report, in priority order: + +1. **Read `[fixtext]` and `[check]` first.** These are the source of truth for what the rule + requires: actual command changes, audit rule format changes, changed values (permissions, + sysctl params, timeouts), new/removed commands. See `reference/02-classify-diffs.md` for how + to tell a real command change from reformatted example output. +2. **Check for a `policy/stig/.yml` override**: + ```bash + find linux_os/guide -path "*//policy/stig/.yml" + ``` + If it exists and mirrors DISA's changed `vuldiscussion`/`fixtext`/`checktext`, update it to + match. If it doesn't exist, a `[description]`-only change needs no action - this project + writes its own description/rationale independently of DISA's wording. +3. **`[title]` changes**: update `title:` only if CaC's title mirrors DISA's wording verbatim. + +If a STIG ID has no existing CaC rule at all, use `find-rule` or `map-requirement` to check +whether an existing rule already covers it before treating it as a new-rule case. diff --git a/.claude/skills/disa-stig-quarterly-update/reference/02-classify-diffs.md b/.claude/skills/disa-stig-quarterly-update/reference/02-classify-diffs.md new file mode 100644 index 000000000000..221d8f239eda --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/02-classify-diffs.md @@ -0,0 +1,97 @@ +# Phase 1: classifying a STIG diff + +Every changed STIG ID gets one classification. Use these labels in the diff report's +`Classification:` field: + +- `prose` - documentation-only (capitalization, punctuation, rephrasing with no meaning change). + No rule update. +- `oval` - the check or fixtext logic changed; the OVAL check and/or remediations need updating. +- `new-rule` - DISA added a requirement with no existing CaC rule covering it. +- `removal` - DISA removed the requirement; drop or mark `status: not applicable` in the + control file. +- `control-file` - the fix is in the control file only (a title correction, an `xccdf_value` + assignment, adding/removing a rule from the selection) with no rule.yml or OVAL change. +- `no-action` - CaC's existing implementation already satisfies the new wording; nothing to do. +- `ocil` - DISA changed manual/OCIL-only text. Red Hat does not maintain OCIL entries; skip. + +## Always needs action (`oval`, `new-rule`, `removal`, or `control-file`) + +- The actual command changed in `[fixtext]` or `[check]` - not example output, the command itself. +- Audit rule format changed: `-w /path -p wa` -> `-a always,exit -F arch=... -F path=... -F perm=...`. +- A sysctl parameter, file path, or permission value changed. +- `[severity]` changed. +- A rule was added (all `+` lines) or removed (all `-` lines) between versions. +- A SELinux filter was added to a check or fixtext (`subj_type=...`). +- A deprecated command was replaced with a newer equivalent (e.g. `awk /etc/passwd` -> + `getent passwd`) - even if the intent is unchanged, the old approach may now miss cases (LDAP/NIS + users, for example) that CaC's remediation should also handle. + +## Skip (`prose` or `no-action`) + +- Capitalization or punctuation only: `"IPsec"` vs `"ipsec"`, a hyphen added or removed, a comma + or trailing period. +- Blank lines added or removed inside `[check]` or `[fixtext]`. +- Example output reformatted (indentation, spacing of command output) with the command itself + unchanged. +- Prose rephrased with no change in meaning. +- Inclusive-language updates: "whitelist" -> "allow list", "Non-privileged" -> "Nonprivileged". +- FIPS 140-2 -> FIPS 140-3 terminology only, unless it changes an actual required value. + +## Read carefully - may or may not need action + +- `[title]` changed: usually prose, but check whether it signals a technical scope change. +- `[description]` changed: usually no action (CaC writes its own rationale); read for + requirement shifts if a `policy/stig/` override exists for that rule. +- `[fixtext]` or `[check]` changed: read the actual command lines, not just the surrounding prose. + +## Distinguishing commands from example output + +`[fixtext]` and `[check]` mix real commands with the output those commands print. A diff on +example output looks identical to a diff on a real command unless you check which is which: + +- Actual commands start with `$` or `#`. +- Example output does not start with `$`/`#`, and follows a command line. + +**No action** - only the `grep` filter and expected output changed, `auditctl -l` itself is the same: +```diff +-$ sudo auditctl -l | grep /etc/cron.d +--w /etc/cron.d -p wa -k cronjobs ++$ sudo auditctl -l | grep crond_t ++-a always,exit -F arch=b64 -S execve -F subj_type=crond_t -F euid=0 -k cron_exec +``` + +**Action needed** - the actual audit rule line changed: +```diff +-[fixtext]: -w /etc/sudoers -p wa -k identity ++[fixtext]: -a always,exit -F arch=b32 -F path=/etc/sudoers -F perm=wa -k identity ++ -a always,exit -F arch=b64 -F path=/etc/sudoers -F perm=wa -k identity +``` + +## Only implement what's in the diff + +Three traps come up repeatedly: + +- Don't add ownership (`chown`) enforcement unless the diff explicitly adds one. "For + completeness" is a reviewer style preference, not a STIG requirement - verify against the raw + diff before acting on it. +- Don't add `stigid@:` to `rule.yml` for RHEL-family products. The STIG ID -> rule + mapping lives in the control file only (`products//controls/*.yml`); `stigid@` in + `rule.yml` is for Oracle Linux and SLE. +- Don't treat an example-output change as a command change (see above). + +## Investigating when there's no obvious mapping + +When a STIG ID has no notes and no obvious existing rule: + +1. Use `find-rule` to search for an existing rule that already implements the requirement text. +2. If nothing matches, use `map-requirement` (single ID) or `map-controls` (batch) for + cross-framework rule suggestions before concluding it's a genuinely new rule. +3. Only fall back to `create-rule` once you've confirmed no existing rule covers it. + +## Implementation flexibility + +There is real wiggle room in how strictly a STIG wording is implemented, particularly for +requirements that resist a strict, checkable definition (a `not applicable` determination doesn't +need to be airtight against DISA's exact phrasing). When a requirement is genuinely ambiguous or +unautomatable, prefer `status: pending` or `status: not applicable` in the control file with a +`notes:` explanation over forcing a rigid check that will false-positive or false-negative. diff --git a/.claude/skills/disa-stig-quarterly-update/reference/03-implement.md b/.claude/skills/disa-stig-quarterly-update/reference/03-implement.md new file mode 100644 index 000000000000..f4c442e316be --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/03-implement.md @@ -0,0 +1,92 @@ +# Phase 2: implement the changes + +## Step 0: swap the reference XML files first + +```bash +git rm shared/references/-xccdf-manual.xml +git add shared/references/-xccdf-manual.xml +``` + +Only one `*-xccdf-manual.xml` per product is allowed in `shared/references/` - the build globs +on `disa-stig-${PRODUCT}-v[0-9]*r[0-9]*-xccdf-manual.xml`, and two matching files break it. Do +this swap as the very first commit on the branch, before any other change. + +If DISA published a new `*-xccdf-scap.xml`, swap that too (needed for the Contest +`disa-alignment` test). If DISA didn't publish one for this release, skip it - the test simply +won't run. + +Also bump the version string in `products//profiles/stig.profile` and +`stig_gui.profile` (`metadata.version`). + +## One commit per STIG ID + +``` +{product}: DISA STIG {version}, {STIG-ID} - {short description} +``` + +Examples: +- `rhel9: DISA STIG v2r9, RHEL-09-255120 - update control file title` +- `rhel9: DISA STIG v2r9, RHEL-09-255130 - remove SSH Compression rule` +- `rhel8: DISA STIG v2r8, RHEL-08-030610 - update audit config file permissions` + +For prose-only changes, use "prose update" as the description: +- `rhel9: DISA STIG v2r9, RHEL-09-671015 - prose update` + +No parentheses, no before/after values in the message unless essential to disambiguate. + +## Before hardcoding a changed value, check for an XCCDF variable + +```bash +grep "xccdf_value" linux_os/guide///rule.yml +``` + +If the rule already exposes a variable, the fix is usually one control-file line, not a rule +edit: + +```yaml +- id: RHEL-09-611010 + levels: [medium] + rules: + - accounts_password_pam_pwquality_retry + - var_password_pam_retry=3 +``` + +Use the `resolve-rule-variables` skill to look up which variables a rule depends on and pick the +right value key. + +## Branch and PR + +- One branch per product (or per product+version if multiple are in flight at once). +- Open the PR as a draft immediately, before all commits land, so reviewers can follow along and + comment early. +- Use `draft-pr` to open it once there's at least one commit - it derives the title, categorizes + changes, and prefills the PR body from `.github/pull_request_template.md`. Its generic + Description/Rationale/Review-Hints sections should be replaced with the STIG-specific content + from `reference/04-pr-description.md` - `draft-pr` doesn't know about STIG IDs or DISA + requirements, this skill does. + +## Shared files (`linux_os/guide/`) go in one PR only + +A prose fix or logic change in a shared rule under `linux_os/guide/` may satisfy the same STIG +ID across multiple products (e.g. a fix that resolves both a RHEL 8 and a RHEL 9 STIG ID). Make +that change in exactly one PR - land it in whichever product's PR is more advanced or more +directly affected - and reference that PR from the other product's PR description. Never +duplicate the change in both branches; it produces a merge conflict when both land on the base +branch. + +Example pattern: a shared `accounts_password_all_shadowed_sha512` prose fix satisfies both +`RHEL-09-671015` and `RHEL-08-010120`. Commit it once in the RHEL 9 PR; the RHEL 8 PR description +notes "prose fix for RHEL-08-010120 covered by PR #N" and links to it. + +## Verifying a change + +- `build-product` to rebuild the affected product's data stream after an OVAL or template change. +- `test-rule` / `run-tests` for rule-level and ctest validation before pushing. +- Contest and CI catch anything a local build/test pass misses; if a Contest failure only + reproduces on CentOS Stream and not on RHEL, that doesn't block STIG compliance work - the STIG + applies to RHEL, and a CentOS-only failure with a known cause can be waived separately (file a + GitHub issue and open a Contest PR to waive it there). + +See `reference/observations.md` for caveats that aren't obvious from the diff or the rule source +(daemons resetting file modes on reboot, SELinux inotify denials, container guards, and the +Contest ansible-vs-bash asymmetry). diff --git a/.claude/skills/disa-stig-quarterly-update/reference/04-pr-description.md b/.claude/skills/disa-stig-quarterly-update/reference/04-pr-description.md new file mode 100644 index 000000000000..93a832898fa8 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/04-pr-description.md @@ -0,0 +1,76 @@ +# Phase 3: PR description + +The diff report already shows WHAT changed. The PR description explains WHY DISA changed the +requirement and what behavior changes in ComplianceAsCode as a result. Don't restate file paths, +XML swaps, or version bumps - those are visible in the diff. + +## Template + +```markdown +## Description: + +Updates the DISA STIG profile to . + +### RHEL-NN-XXXXXX: short description of change + +Framing sentence stating what the section covers and why DISA changed it. + +- Active-voice bullet: "Removes X from Y", not "X was removed from Y". +- Backtick technical names: paths (`shared/references/`), modes (`0640`), variables + (`inactivity_timeout_value`), commands (`cut -d: -f1,2`). + +## Rationale: + +DISA published STIG for ; this PR brings the ComplianceAsCode content in sync. + +## Review Hints: + +[prose if one point; framing sentence + bullets if multiple] +``` + +## Style rules + +**Headings** +- Top-level sections use `##`: `## Description:`, `## Rationale:`, `## Review Hints:`. +- Each STIG ID gets a `###` heading, colon-separated, sentence case: `### RHEL-09-255130: remove + SSH compression rule`. No em dashes. Acronyms stay uppercase (OVAL, SSH, FIPS). + +**References** +- Link every PR number: `[PR #14987](https://github.com/ComplianceAsCode/content/pull/14987)`. + Never a bare `PR #N`. +- Expand abbreviated STIG ID lists: `040221, 040222` -> `RHEL-08-040221, RHEL-08-040222`. + +**Abbreviations** +- Do not expand domain abbreviations: OVAL, DISA, STIG, XCCDF, OCIL, CCI, CCE, SRG, CaC, PAM, + SELinux, NSS, NIS, LDAP, FIPS, NIST, OL, SLE are plain technical terms to this team - never + write "Defense Information Systems Agency (DISA)". +- General terms a non-team reader may not know (e.g. PR) can be expanded on first use. + +**Formatting** +- Active voice, present tense. +- No "Pending (not included)" section. Omit anything not part of this PR. +- Framing sentence before every bullet list. Single-bullet sections become prose. +- No hard-wrapping - one logical line per paragraph or bullet. +- Rationale section is one prose sentence, no bullets. +- Review Hints: prose if there's one point, framing sentence + bullets if there are several. + +## What to explain per STIG ID + +- Why DISA made the change (policy shift, FIPS version bump, new audit standard, a + vendor-support-window update, etc.) - this is almost never visible from the diff alone. +- What behavior changes as a result (e.g. "removes the group-read bit from SSH host keys"). +- The CaC rule name(s) involved. +- For a shared-file change covered in another PR: name that PR and link it explicitly (see + `reference/03-implement.md`). + +## Do not list + +- File paths or XML file swaps - visible in the diff. +- Version bumps - visible in the diff. +- Line-by-line diff content - that's what the diff report from Phase 1 is for. +- Pending or future work. + +## Jira and internal IDs + +Never include an internal issue ID (`OPENSCAP-1234`, `RHEL-12345`, or similar) in the PR title, +body, commits, or comments. This is a public, upstream repository. diff --git a/.claude/skills/disa-stig-quarterly-update/reference/architectural-facts.md b/.claude/skills/disa-stig-quarterly-update/reference/architectural-facts.md new file mode 100644 index 000000000000..9a95bb478ab2 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/architectural-facts.md @@ -0,0 +1,79 @@ +# Architectural facts that affect almost every STIG update + +Misreading these leads to wrong or redundant changes. + +## `audit_watches_style` is a product-level variable + +`audit_watches_style` in `products//product.yml` controls whether audit rules generate +in old `-w` format or new `-a always,exit` format, for the entire product. Setting it to `modern` +satisfies every `-w` -> `-a always,exit` format change at once for rules using the +`audit_rules_watch` template - no per-rule edits needed. It's a build-time variable, not a +per-profile one, so check whether flipping it would also affect non-STIG profiles for the +product before changing it. + +## `audit_rules_watch` template has no `auid` filter support + +It cannot add `auid>=1000` or `auid!=unset` filters. If DISA adds those to a rule's audit line, +the rule needs custom OVAL and Bash/Ansible remediations, not just the `audit_watches_style` flip. + +## `file_permissions` template: `allow_stricter_permissions` + +With `allow_stricter_permissions: true`, modes stricter than `filemode` pass. Tightening +`filemode` (e.g. `0640` -> `0600`) removes previously-passing looser modes - this is the intended +effect when DISA tightens a permission requirement, not a regression. + +`file_permissions` thresholds are per-rule, not per-profile. If two profiles need different +thresholds for the same underlying file, that requires separate rule variants (e.g. a `_stig` +suffix), not a single rule with a profile-conditional value. + +## Control files can set XCCDF variables, not just select rules + +```yaml +- id: RHEL-09-611010 + levels: [medium] + rules: + - accounts_password_pam_pwquality_retry + - var_password_pam_retry=3 # sets the XCCDF variable for this profile +``` + +When a STIG changes a value (timeout, permission mode, threshold), check whether the rule +exposes an `xccdf_value()` (`grep xccdf_value rule.yml`) before hardcoding the new value anywhere +- the fix is often one control-file line. + +## STIG IDs live in the control file only, for RHEL-family products + +`products//controls/stig_.yml` is the only place the STIG ID -> rule mapping +exists. Never add `stigid@: RHEL-NN-XXXXXX` to `rule.yml` - that pattern is used only +for Oracle Linux and SLE (there are zero `stigid@rhel*` instances under `linux_os/guide/`). + +## `policy/stig/.yml` rule overrides + +Some rules have `linux_os/guide///policy/stig/.yml`, overriding prose +specifically for the STIG profile: + +```bash +find linux_os/guide -path "*//policy/stig/.yml" +``` + +If it exists and mirrors DISA's changed `vuldiscussion`/`fixtext`/`checktext`, update it. If it +doesn't exist, a `[description]`-only diff needs no action - CaC writes description/rationale +independently of DISA wording. + +## `sysctl` template OVAL checks runtime + any matching `.conf` file + +The static half of the check covers `/etc/sysctl.conf`, `/etc/sysctl.d/*.conf`, and similar glob +paths - any file in those directories with the correct value passes, regardless of filename. +DISA's fixtext may name a specific file (e.g. `99-kernel_randomize_va_space.conf`) while CaC +writes to a different one (e.g. `kernel_randomize_va_space.conf`) in the same directory - that's +a naming difference, not a functional gap, and needs no action. + +## One reference XML file per product + +The build globs `disa-stig-${PRODUCT}-v[0-9]*r[0-9]*-xccdf-manual.xml`. Two matching files break +`gen_stig_table.py`. Always `git rm` the old one in the same commit that adds the new one - see +`reference/03-implement.md`. + +## Shared rules under `linux_os/guide/` cover multiple products + +A change there can satisfy STIG IDs across several products at once. Land it in one PR only and +reference that PR from the other product's PR description - see `reference/03-implement.md`. diff --git a/.claude/skills/disa-stig-quarterly-update/reference/observations.md b/.claude/skills/disa-stig-quarterly-update/reference/observations.md new file mode 100644 index 000000000000..9bc51a2778e0 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/observations.md @@ -0,0 +1,66 @@ +# Caveats that aren't obvious from the diff or the source + +## Remediations can be undone by system daemons on reboot + +Some daemons overwrite files as part of their startup sequence, resetting modes or content a +remediation just set. Canonical example: `augenrules --load` hardcodes `chmod 0640 +${DestinationFile}` whenever it rewrites `/etc/audit/audit.rules`. A remediation that sets `0600` +survives until the next `auditd` restart that touches the file, at which point `augenrules` +resets it to `0640`. + +General pattern: if a daemon or package post-install script overwrites a file on boot or service +restart, a one-shot remediation won't survive a reboot. Check whether the file is owned by a +daemon that runs on startup; if so, a persistent fix is needed - a systemd dropin, a config file +the daemon reads, or a daemon reload that accepts the new value directly. CaC does not patch +files under `/usr/bin/`, `/usr/sbin/`, `/usr/lib/`, or any other package-managed path; a bug in a +system script belongs upstream, and the CaC remediation must work around it without waiting for +that fix. + +## Contest ansible vs bash asymmetry + +Contest runs ansible once, reboots once, then scans. Bash runs once, reboots, runs again, +reboots again, then scans. The second bash pass can produce a passing scan even when the +underlying problem (a daemon rewriting the file) would fail ansible. A rule that passes in bash +Contest but fails in ansible Contest on the same remediation logic usually means the end state is +correct but doesn't survive one boot cycle without a second pass - the fix is a persistent +mechanism, not reapplying the same one-shot command. + +## SELinux and systemd units in remediations + +An `ExecStartPost=` in a systemd dropin runs in the service's own SELinux domain (e.g. `auditd_t` +for `auditd.service`), not `init_t` - a dropin that `chmod`s or `install`s a file in that domain +needs no custom SELinux policy. + +A `systemd.path` unit with `PathChanged=` is different: the watch is set up by `systemd` (as +`init_t`) calling `inotify_add_watch()`. If the watched directory carries a label like +`auditd_etc_t`, SELinux may deny the `watch` permission for `init_t` - often silently, via a +`dontaudit` rule that won't show in the audit log under normal conditions. To expose it: `semodule +-DB` to disable dontaudit rules, reproduce, then check `ausearch -m avc`. A `PathChanged=` +approach that only works with `setenforce 0` will not survive a reboot, since systemd sets up +path watches during boot while SELinux is already enforcing. + +## Container environments + +`auditd` doesn't run inside containers, and `kernel-core` is absent since containers share the +host kernel. Any remediation installing a systemd dropin or reloading `auditd` must be guarded to +skip that step in containers: `rpm -q kernel-core` in Bash, `"kernel-core" in +ansible_facts.packages` in Ansible. Without the guard, `systemctl` fails to reach a host-side +`auditd` from inside the container. A plain file-mode task (`chmod`, or the `file` Ansible module) +still runs fine in containers and needs no guard. + +## Reviewer style requests vs STIG requirements + +Reviewers sometimes ask for more than the STIG diff requires: extra ownership enforcement, +additional validation, stricter defaults, explanatory comments. Treat these as optional unless +the reviewer says they're blocking. Verify any such request against the raw STIG diff before +implementing it - "for completeness" signals a style preference, "the STIG requires" or "this +blocks merge" signals a hard requirement. Implementing unrequested changes without checking can +introduce scope creep and conflict with other PRs touching the same file. + +## CentOS Stream failures and Contest waivers + +Contest also runs against CentOS Stream in addition to RHEL. A failure that reproduces only on +CentOS Stream, with a known root cause unrelated to RHEL, does not block STIG compliance work - +the STIG applies to RHEL, and RHEL results are the deciding signal. File a GitHub issue and open a +Contest PR to waive the affected test on CentOS; CentOS results after a waiver are informational +only. diff --git a/.claude/skills/disa-stig-quarterly-update/reference/xccdf-format.md b/.claude/skills/disa-stig-quarterly-update/reference/xccdf-format.md new file mode 100644 index 000000000000..e6a3d9bee4b7 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/reference/xccdf-format.md @@ -0,0 +1,78 @@ +# DISA XCCDF format and field mapping + +## DISA publishes two files per release + +- `*-xccdf-manual.xml` - complete requirements with human-readable procedures. "Manual" refers + to the checklist documentation format, not manual remediation steps. Source of truth. +- `*-xccdf-scap.xml` - automated subset containing only rules DISA ships with an OVAL check. Not + every manual-file rule has one, and DISA doesn't always publish this file for a given release. + +## XCCDF structure + +```xml + + + + RHEL-09-211010 + RHEL 9 must be a vendor-supported release. + CCI-000366 + <VulnDiscussion>...</VulnDiscussion> + Steps a sysadmin follows to remediate... + Steps an auditor follows to verify compliance... + + + +``` + +- `Group/@id` - DISA's V-ID, unique across all STIGs, can change between releases. +- `Rule/@id` - internal XCCDF rule ID in `SV-r_rule` format. `compare_ds.py --disa-content` + matches old/new rules by the `SV-` portion, since only the release-number suffix changes + between STIG releases. +- `Rule/@severity` - `low`, `medium`, or `high`. +- `` - the STIG Rule ID (e.g. `RHEL-09-211010`). Product-scoped and stable across + releases; this is the join key with the control file. +- `` - one-line rule name. +- `<description>` - contains `<VulnDiscussion>`, explaining why the requirement exists. +- `<ident system="http://cyber.mil/cci">` - CCI, links to NIST 800-53. +- `<fixtext>` - remediation steps for a sysadmin; source of truth for what CaC should automate. +- `<check>` - verification steps for an auditor; source of truth for what the OVAL check verifies. + +## STIG Rule ID format + +`RHEL-{VERSION}-{CATEGORY}{NUMBER}`, e.g. `RHEL-08-010120`: product, RHEL version, category, +number within category. IDs ending in `0` are typically the original entry; `1`/`2`/`3` variants +are usually related rules in the same group. + +## How `compare_ds.py` produces diffs + +`compare_ds.py --disa-content --rule-diffs` parses each `<Rule>` with `join_text_elements` +(`ssg/xml.py`) into an INI-like block, then unified-diffs old vs new per STIG ID: + +``` +[severity] +[version] +[title] +[description] +[ident] (one block per CCI) +[reference] (benchmark metadata: DISA target, DPMS ID) +[fixtext] +[check] +``` + +`<fix>` elements (embedded Ansible/Bash) are skipped - DISA manual XMLs don't contain them. + +## Mapping DISA fields to ComplianceAsCode + +| DISA field | Becomes | +|---|---| +| `[fixtext]` | Ansible/Bash remediation with the same intent, written as automation | +| `[check]` | OVAL check with the same intent, written as machine-executable XML | +| `[title]` | `title:` in `rule.yml` | +| `[description]` | `description:` and `rationale:` in `rule.yml` | +| `[severity]` | `severity:` in `rule.yml` | +| `[ident]` (CCI) | `references:` in `rule.yml` | +| `[version]` (STIG Rule ID) | `id:` in the control file - not written into the rule itself | +| `[reference]` (DPMS metadata) | not used | + +`[fixtext]` and `[check]` are the source of truth. When they change, the OVAL check and +Ansible/Bash remediations must follow. diff --git a/.claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py b/.claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py new file mode 100644 index 000000000000..5013e41cfdbe --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Turn raw `compare_ds.py --disa-content --rule-diffs` output into one markdown report. + +Each file in the diffs directory is one unified diff for one STIG ID. This script embeds +every diff verbatim inside a collapsible <details> block so the whole report is a single +git-trackable artifact - no external file host, no spreadsheet, no diff2html step. + +Usage: + build_diff_report.py <compare_ds_diffs_dir> <output.md> \\ + --product rhel9 --from-version v2r8 --to-version v2r9 + +The output is a skeleton: fill in CaC rule / Classification / Action for each STIG ID by +hand (see reference/02-classify-diffs.md). Never hand-edit the text inside a ```diff fence - +if a diff looks wrong, re-run compare_ds.py and regenerate the report instead. +""" +import argparse +from pathlib import Path + + +def build(diffs_dir: Path, product: str, from_version: str, to_version: str) -> str: + files = sorted(p for p in diffs_dir.iterdir() if p.is_file()) + if not files: + raise SystemExit(f"no diff files found in {diffs_dir}") + + lines = [ + f"# {product} STIG {from_version} -> {to_version} diff report", + "", + f"{len(files)} STIG IDs changed. Generated from `{diffs_dir}` - every diff below is " + "copied verbatim from compare_ds.py output. Do not hand-edit the fenced blocks; " + "re-run this script against fresh compare_ds.py output instead.", + "", + "---", + "", + ] + for path in files: + stig_id = path.name + diff_text = path.read_text().rstrip("\n") + lines += [ + f"## {stig_id}", + "", + "CaC rule: `TODO`", + "Classification: `TODO` <!-- prose | oval | new-rule | removal | control-file | no-action -->", + "", + "<details>", + f"<summary>Diff: {stig_id}</summary>", + "", + "```diff", + diff_text, + "```", + "", + "</details>", + "", + "Action: TODO", + "", + "---", + "", + ] + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("diffs_dir", type=Path, help="directory of per-STIG-ID diff files") + parser.add_argument("output", type=Path, help="markdown file to write") + parser.add_argument("--product", required=True, help="e.g. rhel9") + parser.add_argument("--from-version", dest="from_version", required=True, help="e.g. v2r8") + parser.add_argument("--to-version", dest="to_version", required=True, help="e.g. v2r9") + args = parser.parse_args() + + report = build(args.diffs_dir, args.product, args.from_version, args.to_version) + args.output.write_text(report) + n = len(list(args.diffs_dir.iterdir())) + print(f"wrote {args.output} ({n} STIG IDs)") + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/disa-stig-quarterly-update/test-fixtures/README.md b/.claude/skills/disa-stig-quarterly-update/test-fixtures/README.md new file mode 100644 index 000000000000..0b660d65c07e --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/test-fixtures/README.md @@ -0,0 +1,47 @@ +# Test fixtures + +A cheap way to exercise the whole assess pipeline - `compare_ds.py` through +`build_diff_report.py` - without downloading a real quarterly release or diffing a +multi-thousand-rule benchmark. + +## What's here + +- `disa-stig-rhel9-test-v1-xccdf-manual.xml` / `-v2-xccdf-manual.xml` - two tiny, one-rule + XCCDF benchmarks. The rule (`RHEL-09-211010`) is lifted verbatim from the real + `shared/references/disa-stig-rhel9-v2r9-xccdf-manual.xml`. `v2` differs from `v1` by exactly + one character: the title's `vendor-supported` loses its hyphen, becoming `vendor supported` - + a punctuation-only change, i.e. a `no-action` classification per + `reference/02-classify-diffs.md`. +- `compare_ds_diffs_sample/RHEL-09-211010` - the real, unmodified output of running + `utils/compare_ds.py --disa-content --rule-diffs` against the two files above. Not + hand-authored - regenerate it any time with the command below to confirm it still matches. + +## Reproduce it + +From the repo root: + +```bash +python3 utils/compare_ds.py --disa-content --rule-diffs \ + --output-dir .claude/skills/disa-stig-quarterly-update/test-fixtures/compare_ds_diffs_sample \ + .claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v1-xccdf-manual.xml \ + .claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v2-xccdf-manual.xml +``` + +Expect exactly one output file, `RHEL-09-211010`, with a one-line diff in `[title]`. + +## Exercise the report builder + +```bash +python3 .claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py \ + .claude/skills/disa-stig-quarterly-update/test-fixtures/compare_ds_diffs_sample \ + /tmp/test-diff-report.md \ + --product rhel9-test --from-version v1 --to-version v2 +``` + +This is the whole Phase 1 output pipeline in about a second, with no need for a real STIG +release, a full-size XML parse, or network access. Add a second rule with a real (not +punctuation-only) change to the fixture pair if you need to test the `oval`/`new-rule`/`removal` +classification paths as cheaply. + +These fixtures are test-only: they never touch `shared/references/`, aren't wired into any +product build, and don't affect real STIG IDs. diff --git a/.claude/skills/disa-stig-quarterly-update/test-fixtures/compare_ds_diffs_sample/RHEL-09-211010 b/.claude/skills/disa-stig-quarterly-update/test-fixtures/compare_ds_diffs_sample/RHEL-09-211010 new file mode 100644 index 000000000000..cbdcd01883b7 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/test-fixtures/compare_ds_diffs_sample/RHEL-09-211010 @@ -0,0 +1,33 @@ +--- RHEL-09-211010 ++++ RHEL-09-211010 +@@ -1,29 +1,29 @@ + [severity]: + high + + [version]: + RHEL-09-211010 + + [title]: +-RHEL 9 must be a vendor-supported release. ++RHEL 9 must be a vendor supported release. + + [description]: + <VulnDiscussion>An operating system release is considered "supported" if the vendor continues to provide security patches for the product. With an unsupported release, it will not be possible to resolve security issues discovered in the system software.</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls> + + [reference]: + DPMS Target Red Hat Enterprise Linux 9DISADPMS TargetRed Hat Enterprise Linux 95551 + + [ident]: + CCI-000366 + + [fixtext]: + Upgrade to a supported version of RHEL 9. + + [check]: + Verify the version or RHEL 9 is vendor supported with the following command: + + $ cat /etc/redhat-release + + Red Hat Enterprise Linux release 9.6 (Plow) + + If the installed version of RHEL 9 is not supported, this is a finding. diff --git a/.claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v1-xccdf-manual.xml b/.claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v1-xccdf-manual.xml new file mode 100644 index 000000000000..f928956128c3 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v1-xccdf-manual.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?><Benchmark xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="RHEL_9_STIG" xml:lang="en" xmlns="http://checklists.nist.gov/xccdf/1.1"><status date="2026-05-20">accepted</status><title>Red Hat Enterprise Linux 9 Security Technical Implementation Guide (test fixture, v1)1SRG-OS-000480-GPOS-00227<GroupDescription></GroupDescription>RHEL-09-211010RHEL 9 must be a vendor-supported release.<VulnDiscussion>An operating system release is considered "supported" if the vendor continues to provide security patches for the product. With an unsupported release, it will not be possible to resolve security issues discovered in the system software.</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>DPMS Target Red Hat Enterprise Linux 9DISADPMS TargetRed Hat Enterprise Linux 95551CCI-000366Upgrade to a supported version of RHEL 9.Verify the version or RHEL 9 is vendor supported with the following command: + +$ cat /etc/redhat-release + +Red Hat Enterprise Linux release 9.6 (Plow) + +If the installed version of RHEL 9 is not supported, this is a finding. diff --git a/.claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v2-xccdf-manual.xml b/.claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v2-xccdf-manual.xml new file mode 100644 index 000000000000..64236401a203 --- /dev/null +++ b/.claude/skills/disa-stig-quarterly-update/test-fixtures/disa-stig-rhel9-test-v2-xccdf-manual.xml @@ -0,0 +1,7 @@ +acceptedRed Hat Enterprise Linux 9 Security Technical Implementation Guide (test fixture, v2)2SRG-OS-000480-GPOS-00227<GroupDescription></GroupDescription>RHEL-09-211010RHEL 9 must be a vendor supported release.<VulnDiscussion>An operating system release is considered "supported" if the vendor continues to provide security patches for the product. With an unsupported release, it will not be possible to resolve security issues discovered in the system software.</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>DPMS Target Red Hat Enterprise Linux 9DISADPMS TargetRed Hat Enterprise Linux 95551CCI-000366Upgrade to a supported version of RHEL 9.Verify the version or RHEL 9 is vendor supported with the following command: + +$ cat /etc/redhat-release + +Red Hat Enterprise Linux release 9.6 (Plow) + +If the installed version of RHEL 9 is not supported, this is a finding. From e88b5518da62807a13edcdb883d982a70c7e35cb Mon Sep 17 00:00:00 2001 From: Peter Macko <44851174+macko1@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:35:23 +0200 Subject: [PATCH 2/6] Update disa-stig-quarterly-update skill --- .../disa-stig-quarterly-update/SKILL.md | 88 ++++++++++++----- .../reference/01-assess.md | 87 +++++++++-------- .../reference/03-implement.md | 65 ++++++++++--- .../reference/04-pr-description.md | 6 +- .../scripts/build_html_diffs.py | 55 +++++++++++ .../scripts/build_review_csv.py | 97 +++++++++++++++++++ .../test-fixtures/README.md | 23 +++-- 7 files changed, 335 insertions(+), 86 deletions(-) create mode 100644 .claude/skills/disa-stig-quarterly-update/scripts/build_html_diffs.py create mode 100644 .claude/skills/disa-stig-quarterly-update/scripts/build_review_csv.py diff --git a/.claude/skills/disa-stig-quarterly-update/SKILL.md b/.claude/skills/disa-stig-quarterly-update/SKILL.md index e1621084d818..13b71f94e2b8 100644 --- a/.claude/skills/disa-stig-quarterly-update/SKILL.md +++ b/.claude/skills/disa-stig-quarterly-update/SKILL.md @@ -5,10 +5,15 @@ description: Assess, implement, and describe a DISA STIG quarterly benchmark upd # DISA STIG quarterly update +This skill produces a reviewable, pushed update branch for each RHEL product in scope. The MVP +expects the user to download the DISA manual XML files. The model runs the comparison tools, +retains every intermediate artifact, implements approved changes commit by commit, pushes the +branches, and writes Markdown PR drafts. It does not open GitHub PRs automatically. + Three phases: **assess** the new release, **implement** the changes, **describe** them in the PR. Each phase has its own reference doc; read only the one you're on. -- `reference/01-assess.md` - download, diff with `compare_ds.py`, build the diff report +- `reference/01-assess.md` - locate inputs, diff with `compare_ds.py`, build review artifacts - `reference/02-classify-diffs.md` - prose vs OVAL vs new-rule vs removal vs control-file - `reference/03-implement.md` - commits, branches, PR, shared-file cross-PR pattern - `reference/04-pr-description.md` - PR description template and style @@ -19,6 +24,35 @@ PR. Each phase has its own reference doc; read only the one you're on. - `reference/observations.md` - daemons resetting file modes on reboot, SELinux inotify denials, Contest ansible/bash asymmetry, container guards +## MVP result layout + +Keep a separate work package for each product. Do not delete a work package when a later step +fails; partial results are useful for review and debugging. + +```text +/ + rhel8-v2r7-to-v2r8/ + compare_ds/ + stdout.txt + diffs/ + html_diffs/ + csv/ + review.csv + assessment/ + diff_report.md + action_table.md + pr/ + description.md + rhel9-v2r8-to-v2r9/ + ... +``` + +The normalized CSV is the primary human-review result for the MVP. It uses these review columns: + +```text +Requirement,HTML diff URL,STDOUT from compare_ds.py,Changes,Action Required,notes,Assignee,Status,Link,Pull request,model-proposed-changes +``` + ## Hard rules - **Diffs are copied verbatim, never from memory.** Every diff embedded in a report, analysis, @@ -34,20 +68,19 @@ PR. Each phase has its own reference doc; read only the one you're on. (`products//controls/*.yml`). Never add `stigid@:` to `rule.yml` there - that's for Oracle Linux and SLE. - **One `*-xccdf-manual.xml` per product** in `shared/references/`. Swap it with `git rm` + - `git add` as the first commit, not two files coexisting. + `git add` as the final implementation commit, not two files coexisting. - **No pending-work sections in PR descriptions.** Describe only what the PR implements. +- **Do not open GitHub PRs automatically.** Push the branches and write `pr/description.md`; + opening the PR is a separate user action. -## The diff report replaces spreadsheet tracking and file-server hosting +## Assessment artifacts -The old process for this copy-pasted changed STIG IDs into a shared spreadsheet and `scp`'d -`diff2html` output to an internal file server, linking back to it from the spreadsheet. Neither -belongs to a change that lives entirely in this repo - a spreadsheet needs someone to have an -account and keep tabs and links in sync by hand, and a file-server upload is invisible to anyone -reviewing the PR itself. +The MVP keeps the existing spreadsheet and HTML-review workflow, but makes every input and output +reproducible in the work package. The CSV is the spreadsheet upload. The HTML files are retained +for publication on the review host. Markdown is retained as the detailed local assessment. -`scripts/build_diff_report.py` replaces both: it turns raw `compare_ds.py --disa-content ---rule-diffs` output into one markdown file per product, with every raw diff embedded verbatim -in a collapsible section, ready to read (and review) inside the PR: +`scripts/build_diff_report.py` turns raw `compare_ds.py --disa-content --rule-diffs` output into +one Markdown report per product, with every raw diff embedded verbatim in a collapsible section: ```bash python3 .claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py \ @@ -55,6 +88,16 @@ python3 .claude/skills/disa-stig-quarterly-update/scripts/build_diff_report.py \ --product rhel9 --from-version v2r8 --to-version v2r9 ``` +Generate the retained HTML and normalized CSV beside it: + +```bash +python3 .claude/skills/disa-stig-quarterly-update/scripts/build_html_diffs.py \ + +python3 .claude/skills/disa-stig-quarterly-update/scripts/build_review_csv.py \ + \ + --html-base-url +``` + Output shape (one section per STIG ID): ```markdown @@ -77,12 +120,12 @@ Classification: `TODO`