| name | secure-rpm-audit |
| description | Use when the user asks to perform a security audit, security review, or vulnerability assessment of an RPM packaging repository (CentOS Stream / Fedora / RHEL dist-git) — a repo containing a .spec file, downstream patches, and a sources lookaside manifest — using OWASP ASVS, the SEI CERT C/C++ Coding Standards, Fedora Packaging Guidelines, SLSA, and OpenSSF Scorecard. |
Secure RPM Audit
Perform a comprehensive security assessment of one or more RPM dist-git packaging repositories. The assessment covers both the packaging layer (spec file, scriptlets, file permissions, hardening macros, patches, source integrity) and the prepared upstream source tree (the upstream tarball with all downstream patches applied), using OWASP ASVS v5.0, the SEI CERT C and C++ Coding Standards, the Fedora Packaging Guidelines, SLSA v1.2, and OpenSSF Scorecard.
Input
$ARGUMENTS is one of the following:
-
A single GitLab dist-git URL — e.g.
https://gitlab.com/redhat/centos-stream/rpms/389-ds-base/-/tree/c10s
Parse the component name from the path segment after /rpms/ and the branch from the segment after /-/tree/. If no branch is present in the URL, default to c10s.
-
Component shorthand — <component> <stream> (e.g. 389-ds-base c10s). Construct the repository URL as:
https://gitlab.com/redhat/centos-stream/rpms/<component>
and use <stream> as the branch. If <stream> is omitted, default to c10s.
-
A CSV batch file with the following schema:
Component, GitLab URL, Branch, Category, SRPM Name, Image Count
- Use the GitLab URL column as the repository source.
- Use the Branch column as the ref to analyze. If the value starts with
commit:, treat the remainder as a commit SHA.
- Use the Category and Image Count columns for prioritization (see below).
Repository Access
Prefer the GitLab MCP tools and WebFetch to read packaging files remotely whenever possible. Raw file URLs follow the pattern:
https://gitlab.com/redhat/centos-stream/rpms/<component>/-/raw/<branch>/<file>
Files to retrieve for every component:
<component>.spec (or *.spec if the name differs)
sources
- All
*.patch files
gating.yaml
rpminspect.yaml
*.sysusers, *.tmpfiles, *.service, *.socket, *.timer
.fmf/ and *.fmf
changelog (if separate from the spec)
When a full local checkout is required for deep analysis (e.g. running static analysis tools on the prepared source tree, or applying patches that the remote view cannot resolve):
git clone --depth 1 --branch <branch> https://gitlab.com/redhat/centos-stream/rpms/<component>.git <local-path>
If the ref is a commit SHA rather than a branch name, clone with --depth 1 and then git fetch origin <sha> && git checkout <sha>.
Always analyze the branch or commit ref specified in the input, not the repository default branch. The ref represents the exact packaging shipped in the target stream.
Source Preparation
The dist-git repository contains packaging metadata only. To audit the code that actually ships in the binary RPM you must reconstruct the prepared source tree:
-
Parse the spec file for Name, Version, Release, every SourceN: line, every PatchN: line, and the %prep section (note whether %autosetup, %autopatch, or explicit %patchN directives are used, and the -p strip level).
-
Read the sources manifest. Each line has the form:
SHA512 (<filename>) = <128-hex-digest>
-
Fetch every source artifact. For each entry in sources, try the CentOS Stream lookaside cache first:
https://sources.stream.centos.org/sources/rpms/<component>/<filename>/sha512/<digest>/<filename>
If the lookaside fetch fails, fall back to the literal URL given in the corresponding SourceN: line of the spec.
-
Verify integrity. Compute the SHA512 of every fetched artifact and compare it against the sources manifest. Any mismatch is a Critical finding (RPM06, CWE-494). Any SourceN tarball that is not listed in sources at all is a High finding.
-
Build the prepared tree. Extract the primary tarball (Source0) into a working directory and apply every PatchN in numeric order using the strip level indicated by %prep. If secondary sources (Source1..N) are vendored dependency bundles (e.g. vendor-*.tar.gz, Cargo-*.lock, node_modules-*.tar.gz), extract them alongside Source0 so dependency scanners can reach them.
The resulting directory is the prepared source tree — the exact code compiled into the shipped RPM — and is the target of the OWASP ASVS review below.
Deduplication
Before beginning analysis, deduplicate the input list by (Component, Branch) pair. If the same component at the same ref appears multiple times:
- Analyze it once.
- Place the canonical report in the first stream's output directory (alphabetical).
- Create symbolic links from every other stream directory that shares the same component+ref back to the canonical report.
Prioritization
Process components in the following order:
- Newest stream first —
c10s, then c9s, then c8s.
- Within a stream, by CSV
Category if provided:
- Core Platform (kernel, glibc, systemd, openssl, …)
- Security Libraries (nss, gnutls, krb5, pam, …)
- Network Services (httpd, bind, openssh, 389-ds-base, …)
- Other
- Within a category, by
Image Count descending (more downstream consumers = larger blast radius).
Security Assessment Framework
OWASP ASVS (Prepared Source Tree)
Using the OWASP ASVS v5.0 (CSV reference) as the primary application security framework, perform a comprehensive security review of the prepared source tree covering:
- Insecure coding practices
- Improper input sanitization
- SSRF / CSRF
- Confused deputy vulnerabilities
- SQL injection and other injection classes
- Credential leaks and secrets in source
- Vulnerable dependencies
When a finding originates in upstream code, note in the finding description whether any downstream .patch already addresses it.
SEI CERT C / C++ Coding Standards (Prepared Source Tree)
When the prepared source tree contains C or C++ code, assess it against the SEI CERT C Coding Standard and the SEI CERT C++ Coding Standard. Focus on the rule categories with the highest security impact:
| Standard | Rule Categories | What to Check |
|---|
| CERT C | STR (Strings) | Unbounded string copies (strcpy, strcat, sprintf, gets), missing null-termination, off-by-one in buffer sizing, format-string injection (STR30-C, STR31-C, STR32-C, FIO30-C) |
| CERT C | MEM (Memory) | Use-after-free, double-free, uninitialized reads, mismatched allocation/deallocation, leaking sensitive data via uncleared buffers (MEM30-C, MEM31-C, MEM34-C, MEM03-C) |
| CERT C | INT (Integers) | Signed overflow, unsigned wrap used in size calculations, truncation on narrowing conversion, tainted values used as array indices or allocation sizes (INT30-C, INT31-C, INT32-C, ARR30-C) |
| CERT C | FIO / POS (I/O & POSIX) | TOCTOU races on file paths, operating on files in shared directories without O_NOFOLLOW/O_EXCL, following symlinks across privilege boundaries, unchecked return values from privileged syscalls (FIO45-C, POS35-C, POS36-C, POS37-C) |
| CERT C | ENV / SIG / CON (Environment, Signals, Concurrency) | Calling system() or exec*() with tainted input, trusting getenv() in setuid contexts, async-signal-unsafe functions in signal handlers, data races on shared state (ENV33-C, ENV03-C, SIG30-C, CON43-C) |
| CERT C++ | MEM / EXP / OOP | new/delete vs new[]/delete[] mismatch, raw owning pointers without RAII, dereferencing dangling references or iterators, slicing of polymorphic objects, deleting through a base pointer without a virtual destructor (MEM51-CPP, EXP54-CPP, OOP52-CPP) |
| CERT C++ | CTR / STR (Containers & Strings) | Iterator invalidation after container mutation, out-of-range element access, forming pointers/references past the end, range errors on std::string/std::string_view (CTR51-CPP, CTR52-CPP, STR53-CPP) |
| CERT C++ | ERR / CON (Errors & Concurrency) | Exceptions escaping destructors or noexcept functions, catching by value, unjoined/un-detached std::thread, data races and deadlocks on shared state (ERR50-CPP, ERR58-CPP, CON50-CPP, CON52-CPP) |
For each finding, record the specific CERT rule ID (e.g. STR31-C, MEM51-CPP) in the finding's category field alongside the mapped CWE. Use the CERT risk-assessment triple (Severity / Likelihood / Remediation Cost) to inform the finding's severity and CVSS score, and note in the description whether any downstream .patch already mitigates the issue.
RPM Packaging Security (Spec File & Dist-Git Artifacts)
Using the Fedora Packaging Guidelines and the Fedora Security Hardening Flags as the packaging security framework, assess the spec file and every artifact in the dist-git repository against the following risk categories:
| ID | Risk Category | What to Check |
|---|
| RPM01 | Scriptlet Injection & Unsafe Shell | %pre / %post / %preun / %postun / %pretrans / %posttrans / %trigger* / %filetrigger* — unquoted variable expansion, eval on external input, command substitution on user-controlled paths, writes to /tmp or /var/tmp without mktemp, rm -rf on unvalidated paths, missing exit-status checks, scriptlets that modify files outside the package's own paths |
| RPM02 | Privileged File Modes & Capabilities | %attr / %defattr granting setuid (4xxx) or setgid (2xxx) bits, world-writable files or directories (xx2/xx6/xx7), %caps(...) granting Linux capabilities (esp. cap_sys_admin, cap_net_admin, cap_dac_override, cap_setuid), %verify(not ...) exclusions that would hide post-install tampering, %ghost files in security-sensitive locations |
| RPM03 | Hardening Macro Overrides | %undefine _hardened_build, %global _hardened_build 0, %define _fortify_level 0, %undefine _fortify_level, %global __brp_strip %{nil}, %global _lto_cflags %{nil}, custom CFLAGS/LDFLAGS that drop -fstack-protector-strong, -D_FORTIFY_SOURCE, -fPIE, -Wl,-z,relro, -Wl,-z,now, or -fcf-protection |
| RPM04 | Systemd Unit Hardening | For every *.service / *.socket / *.timer shipped in %files or installed in %install: missing NoNewPrivileges=yes, ProtectSystem=, ProtectHome=, PrivateTmp=yes, PrivateDevices=yes, MemoryDenyWriteExecute=yes, RestrictSUIDSGID=yes, SystemCallFilter=, CapabilityBoundingSet=; User=/Group= absent (running as root) without justification; ExecStart= invoking interpreters on world-writable paths |
| RPM05 | User & Filesystem Provisioning | sysusers.d entries with a login shell other than /sbin/nologin or /usr/sbin/nologin, UID 0, or a home directory under a world-writable path; tmpfiles.d entries creating world-writable files/directories, symlinks pointing into user-controlled locations, or files with mode >0755 outside /run |
| RPM06 | Source Integrity | SourceN: URLs using http:// or ftp:// instead of https://; source tarballs referenced in the spec but absent from the sources SHA512 manifest; SHA512 digest mismatch between fetched artifact and sources; %global _disable_source_fetch 0 or any build-time download of unverified content |
| RPM07 | Patch Provenance & Regression | .patch files lacking an upstream issue/PR/commit reference in the header; patches that remove input validation, authentication checks, bounds checks, or cryptographic verification; patches that disable or skip tests; PatchN: declared but never applied in %prep; patches applied with -p0 to paths outside the source tree |
| RPM08 | Vendored Dependency Drift | Bundled dependency archives in sources (e.g. vendor-*.tar.gz, Cargo-*.lock, go-vendor-*.tar.gz, node_modules-*.tar.gz) — extract the lock/manifest and run cargo audit, govulncheck, npm audit, or osv-scanner against it; Provides: bundled(...) declarations without a version; bundled libraries with known CVEs not patched downstream |
| RPM09 | Build-Time Network & Privilege | %prep / %build / %install / %check invoking curl, wget, git clone, pip install, go get, npm install, cargo fetch (network access during build); use of sudo or su; writes outside %{buildroot} during %install; %check disabled (%global _without_check 1, empty %check, or ` |
| RPM10 | Gating, Inspection & Test Coverage | Missing or empty gating.yaml; rpminspect.yaml waiving security-relevant inspections (badfuncs, runpath, elf, permissions, capabilities, setuid, securitypolicy); no .fmf test plan or tests/ directory; gating decision context not requiring osci.brew-build.tier0.functional or equivalent |
For each RPM01–RPM10 finding, reference the specific RPM risk ID in the finding's category field alongside the CWE and CVSS score.
SLSA & OpenSSF Scorecard (Supply Chain Integrity)
Using SLSA v1.2 and OpenSSF Scorecard as supply chain assessment frameworks, evaluate the component's build and release integrity:
| Area | What to Check |
|---|
| SLSA Build Provenance | Whether the SRPM build generates signed provenance, whether Koji/Brew build is hermetic, whether the lookaside cache is the sole source input (SLSA L1–L3) |
| Source Pinning | Every SourceN listed in sources with a SHA512 digest; no floating latest or unversioned URLs in SourceN: lines |
| Vendored Dependency Pinning | Cargo.lock / go.sum / package-lock.json present and pinned by hash for every vendored language ecosystem |
| Image / Artifact Signing | Built RPMs signed with the distribution GPG key; Sigstore/cosign attestations if the component also ships container images |
| CI/CD Security | gating.yaml requires passing tests before compose; no rpminspect.yaml waivers that bypass security checks; .fmf plans do not run untrusted code from PRs |
| Vulnerability Disclosure | SECURITY.md present in the upstream repository (Source0 host) with clear reporting instructions |
| Scorecard Checks | If an OpenSSF Scorecard is available for the upstream project (via api.securityscorecards.dev), include the overall score and flag any checks scoring below 5/10 |
Reference SLSA levels (e.g. SLSA L1, SLSA L2) and Scorecard check names (e.g. Pinned-Dependencies, Signed-Releases) in findings.
Existing Scanner Results
Include any findings that already exist for the component from automated scanning tools and trackers:
- Red Hat Bugzilla / Jira — open CVE flaw bugs filed against the component (
component=<name>, keyword Security)
- OSV.dev — query by the upstream package ecosystem and version parsed from
Source0
- rpminspect — if a baseline
rpminspect.yaml exists, note any waived findings and their justification
- govulncheck / cargo audit / npm audit / osv-scanner — run against vendored lock files extracted in Source Preparation
- Dependabot / Renovate — if the upstream
Source0 repository has alerts or open security PRs
- Any
.snyk, .trivyignore, or similar policy files in the prepared source tree
Output
Repository Layout
The validation scripts, renderer, and JSON schema are bundled in this repository:
ai-security-harness/
├── scripts/
│ ├── validate_report.py # JSON Schema + cross-validation checks
│ └── render_report.py # JSON → Markdown renderer (post-processing)
├── schema/
│ └── report.schema.json # authoritative report schema
└── findings/ # report output directory
Report Placement
Place each report in the findings/ directory within this repository:
findings/<stream>/<component>/<component>-security-audit.json
Where:
<stream> is the dist-git branch analyzed (e.g. c10s, c9s, c8s).
<component> is the source package name (the Name: field from the spec, e.g. 389-ds-base).
For deduplicated components shared across streams, place the canonical report under the first stream alphabetically and create symbolic links from the others:
findings/c10s/389-ds-base/389-ds-base-security-audit.json (canonical)
findings/c9s/389-ds-base/389-ds-base-security-audit.json (symlink → canonical)
Report File Name
Each report file must be named: <component>-security-audit.json
Finding IDs
Every finding id must be globally unique across the entire campaign, not just within its own report, so that a finding can be located by ID alone without first knowing which component it belongs to. Use the canonical format:
{COMPONENT_SLUG}-{SHORTSHA}-{NNN}
| Component | Derivation |
|---|
COMPONENT_SLUG | The dist-git component name (spec Name: field), uppercased, with every character outside [A-Z0-9] replaced by _, truncated to at most 24 characters. E.g. 389-ds-base → 389_DS_BASE; NetworkManager → NETWORKMANAGER; python-cryptography → PYTHON_CRYPTOGRAPHY. |
SHORTSHA | The first 7 lowercase hex characters of the dist-git commit SHA being audited — the same value written to metadata.commit. If the input specified a branch rather than a commit, resolve it: git rev-parse --short=7 HEAD after checkout, or via the GitLab MCP tools. |
NNN | Three-digit zero-padded sequence starting at 001 and incrementing per finding within this report. |
Examples: 389_DS_BASE-4f9e812-003 · OPENSSL-9cb7556-012 · NETWORKMANAGER-a1b2c3d-001.
Always populate metadata.commit with the full 40-char SHA (or at minimum the same 7-char short SHA) so the ID is reproducible and the finding can be traced to an exact packaging state. The regex the schema and validator enforce for reports produced by harness ≥ 0.12.0 is:
^[A-Z][A-Z0-9_]{0,23}-[a-f0-9]{7}-\d{3}$
Note: the schema anchor requires the slug to start with [A-Z]. For components whose name begins with a digit (e.g. 389-ds-base), prefix the slug with RPM_: 389-ds-base → RPM_389_DS_BASE.
Reports produced by earlier harness versions may carry legacy IDs (FIND-001, etc.); these still pass schema validation but draw a strict-mode warning. Do not emit legacy IDs in new reports.
Report Format
Reports are JSON files validated against schema/report.schema.json in this repository. The validator script scripts/validate_report.py enforces both the JSON Schema and additional cross-validation checks (ID uniqueness, severity count consistency, cross-references between sections). Do not produce Markdown — Markdown rendering is a separate post-processing step via scripts/render_report.py.
Report Structure Reference
The JSON report has the following top-level keys. The schema (schema/report.schema.json) is the authoritative source of truth for types, constraints, and optional fields.
Required keys:
| Key | Description |
|---|
title | Report title. Must contain a security-related keyword: security, audit, assessment, review, finding, or analysis. |
metadata | Object with required date (YYYY-MM-DD) and scope (≥10 chars). Optional: repository, commit, framework, auditor, methodology, tools (array), loc_reviewed, additional (object). Must include harness_version — read the semver from the VERSION file at the root of the ai-security-harness repository and append the short git SHA (e.g., 0.1.0-4dd9796). For RPM audits, set repository to the GitLab dist-git URL and record the upstream Source0 URL, spec Version/Release, and the list of applied patches under additional. |
executive_summary | Object with prose (≥50 chars) and severity_counts (object with required critical, high, medium, low integer counts). Optional: key_risks (array of strings), positive_observations (array of strings). |
severity_criteria | Array of ≥4 entries, each with level (critical/high/medium/low/informational) and definition (≥20 chars). Must include at least critical, high, medium, and low. |
findings | Array of finding objects (see below). |
findings_summary | Array of ≥4 severity count entries, each with severity, count, and finding_ids. Counts and IDs must match the actual findings. |
remediation_roadmap | Array of ≥1 items, each with priority, action (≥10 chars), and addresses (array of finding IDs). |
Finding object fields:
| Field | Required | Description |
|---|
id | Yes | Globally-unique ID in the form {COMPONENT_SLUG}-{SHORTSHA}-{NNN} (see Finding IDs above). Must be unique across the report and across the campaign. |
title | Yes | Short description (≥5 chars). |
severity | Yes | One of: critical, high, medium, low, informational. |
cwes | Yes | Array of ≥1 CWE identifiers matching CWE-NNN (e.g. CWE-269). |
locations | Yes | Array of ≥1 objects with required path and optional lines, description. For packaging findings, path is the dist-git file (e.g. 389-ds-base.spec); for source findings, path is relative to the prepared source tree. |
description | Yes | Detailed description of the vulnerability (≥50 chars). |
remediation | Yes | Remediation guidance (≥10 chars). |
cvss | No | Object with score (0.0–10.0) and vector string. |
capec | No | Array of CAPEC IDs matching CAPEC-NNN. |
evidence | No | Array of code blocks, each with required code and optional language, caption. |
attack_pattern | No | Description of the attack scenario. |
category | No | Finding category. For packaging findings use the RPM01–RPM10 ID (e.g. RPM02: Privileged File Modes & Capabilities). |
asvs_references | No | Array of ASVS requirement IDs. |
validation_status | No | One of: confirmed, corrected, false_positive, not_verified, hardening (ledger-set, never at audit time). |
source_findings | No | Array of source scanner finding IDs (Bugzilla, OSV, rpminspect, etc.). |
Optional top-level keys:
| Key | Description |
|---|
dependency_audit | Object with optional prose and entries array (each: package, version, status, optional notes). Populate from BuildRequires/Requires and vendored lock files. |
negative_results | Array of areas verified clean, each with area, result, optional files. |
asvs_coverage | Array of ASVS chapter coverage entries. |
scanner_correlation | Array of scanner tool results. |
footer | Free-text footer string. |
Report Validation
After writing the JSON report, you must validate it before considering the report complete. Do not move on to the next component until the current report passes validation with 0 errors.
Step 1 — Run the validator:
python scripts/validate_report.py <path-to-report.json>
The validator must exit with Result: ALL PASSED and 0 errors.
Step 2 — Fix any errors and re-validate:
If errors appear, read each error message, fix the JSON, and re-run the validator. Repeat until 0 errors. Common error categories:
- Schema violations — missing required fields, wrong types, values too short, pattern mismatches (e.g. CWE format)
- Cross-validation errors —
findings_summary counts don't match actual findings, finding_ids reference nonexistent IDs, remediation_roadmap addresses unknown findings, duplicate finding IDs, missing mandatory severity criteria levels
Step 3 — Run strict mode (recommended):
python scripts/validate_report.py --strict <path-to-report.json>
Strict mode surfaces warnings for missing recommended sections (dependency_audit, negative_results, footer, CVSS scores on findings, positive_observations). Address these warnings when the source material supports it.
Step 4 — Render Markdown:
After validation passes, render a human-readable Markdown version of the report:
python scripts/render_report.py <path-to-report.json> -o <path-to-report.md>
The output file should be placed alongside the JSON report with the same base name but a .md extension:
findings/<stream>/<component>/<component>-security-audit.md
This Markdown file is for human consumption only — the JSON file remains the authoritative report artifact.
A report is not complete until scripts/validate_report.py exits with 0 errors. Do not begin analysis of the next component until the current report passes validation.