Skip to content

Commit 87699ff

Browse files
author
wildleo91
committed
chore(security): round 8 — recurring guards + supply-chain signing (build 629)
Tier 1.1 (verified live): FIM coverage end-to-end. Probe write to a WATCH_CODE file produced both a fim_watch_file_modified event in ~1s and a fim_file_modified event in ~8s. Confirms round 6/7 additions (recovery scripts, package.sh, _recovery_log.jsonl) actually fire — not just look right in static review. Tier 1.2: .append() / $(htmlString) audit. 62 jQuery DOM-injection sinks beyond .html() audited (40 + 3 + 3 + 1 + 5 + 10). Zero XSS bugs. Same project-wide _.escape convention as round 7 C3 found. Documented in HTML_INJECTION_AUDIT.md as the round-8 extension. Tier 1.3: Sigstore keyless signing of .spl + per-release SBOM in release.yml. Uses the workflow's OIDC token (id-token: write) to mint short-lived ephemeral keys via Fulcio, records signatures in the public Rekor transparency log. Closes the previously- documented gap where .sha256 + .spl shared a single channel. Verifier command included in workflow comments. Tier 1.4: Quarterly pip-audit CI workflow (Jan/Apr/Jul/Oct 1 at 09:00 UTC). Fails on any vulnerability, surfacing via GitHub's existing notification settings. Origin: round 7 B4 was a one-off run; without recurrence we'd forget. Tier 2.5: Per-release SBOM generation. scripts/generate_sbom.py extracts the .spl, hashes every bundled file, emits CycloneDX 1.5 JSON. package.sh now calls it as step 6/6. Replaces the static sbom.cdx.json baseline with a per-release artifact that matches the .spl byte-for-byte. Tier 2.6: Long-term archival guidance in indexes.conf — example coldToFrozenScript stanza + extended-online-retention alternative + Splunk doc pointers. No default changed; guidance only for compliance regimes (PCI/HIPAA/SOX) needing >3 years. Operational: scheduled Q3 2026 Splunk version-pinning audit (run_once_at: 2026-07-18T07:00:00Z, routine trig_01QE78KzCtSTuwFv2LjrUQqC) — re-runs pip-audit, scans CVEs, checks 10.x compat, opens a PR with findings. Doc cleanup: action=fim_file_modified is the actual wire-level name (prior CHANGELOG entries called it fim_code_modified). Fixed in this round's notes; prior entries left untouched as historical record. Build 628 -> 629. Files: 9 (7 modified, 2 added).
1 parent 2c5cc1b commit 87699ff

9 files changed

Lines changed: 518 additions & 28 deletions

File tree

.github/workflows/pip-audit.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: pip-audit — dev dependency CVE scan
2+
3+
# Quarterly CVE scan of `requirements-dev.txt` against the OSV
4+
# database + GitHub Advisory Database. Origin: round 7 B4
5+
# (one-off run, 2026-04-29) found pytest 8.1.1 vulnerable to
6+
# GHSA-6w46-j5rx-g56g. Round 8 (2026-04-29) wires this as a
7+
# recurring CI job so we don't forget to re-run.
8+
#
9+
# Cadence: 1st of every quarter (Jan 1, Apr 1, Jul 1, Oct 1) at
10+
# 09:00 UTC. Also runs on demand via workflow_dispatch.
11+
#
12+
# Failure mode: the workflow fails when pip-audit finds a vuln,
13+
# producing a red CI badge + email to the repo owner via GitHub's
14+
# existing notification settings. No PR is auto-created — the
15+
# remediation requires human judgment (bump pin? wait for upstream
16+
# fix? add a known-issue exception?).
17+
18+
on:
19+
schedule:
20+
# Quarterly: Jan 1, Apr 1, Jul 1, Oct 1 at 09:00 UTC.
21+
# Pinned to the 1st (not Sunday-of-week) so audits anchor on
22+
# absolute dates that are easy to reason about against the
23+
# CHANGELOG and version-pinning audit cadence.
24+
- cron: '0 9 1 1,4,7,10 *'
25+
workflow_dispatch:
26+
27+
permissions:
28+
contents: read
29+
30+
jobs:
31+
pip-audit:
32+
name: Audit requirements-dev.txt
33+
runs-on: ubuntu-latest
34+
permissions:
35+
contents: read
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: "3.11"
43+
44+
- name: Install pip-audit
45+
run: |
46+
python -m pip install --upgrade pip
47+
pip install pip-audit==2.7.3
48+
49+
- name: Run pip-audit
50+
# `--strict` exits non-zero on ANY finding (including those
51+
# without a known fix). Use `--ignore-vuln <ID>` here to
52+
# carve out a documented exception with a tracking note.
53+
run: |
54+
pip-audit --requirement requirements-dev.txt --strict
55+
56+
- name: Reminder on failure
57+
if: failure()
58+
run: |
59+
echo "::error::pip-audit found at least one vulnerability."
60+
echo "::error::Document the finding in docs/PIP_AUDIT_LOG.md and either"
61+
echo "::error::bump the pin or add an --ignore-vuln <ID> exception with rationale."

.github/workflows/release.yml

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ jobs:
1111
release:
1212
name: Build and attach .spl
1313
runs-on: ubuntu-latest
14-
# Per-job permissions (round 7, 2026-04-29). `contents: write` is
15-
# the minimum to upload .spl + .sha256 assets to a published
16-
# release via `gh release upload`. No other scopes needed —
17-
# explicitly NOT requesting `actions:`, `packages:`, `id-token:`
18-
# so a future workflow-level permissions widening can't silently
19-
# enrich this job's token.
14+
# Per-job permissions (round 7 + round 8, 2026-04-29).
15+
# - `contents: write` — upload .spl + .sha256 + .sig + .crt as
16+
# release assets via `gh release upload`.
17+
# - `id-token: write` — mint the short-lived OIDC token Sigstore
18+
# uses to prove this run came from THIS workflow at THIS commit
19+
# for THIS tag. Required for cosign keyless signing. Without it
20+
# cosign falls back to interactive browser flow (impossible
21+
# in CI) and the step fails closed.
2022
permissions:
2123
contents: write
24+
id-token: write
2225
steps:
2326
- uses: actions/checkout@v4
2427

@@ -33,10 +36,62 @@ jobs:
3336
- name: Build .spl package
3437
run: bash scripts/package.sh
3538

36-
- name: Attach .spl to release
39+
# Round 8 (2026-04-29) — Sigstore keyless signing.
40+
#
41+
# Why this exists: the .spl + .sha256 ride the same channel
42+
# (GitHub Releases). A Releases takeover for this repo would
43+
# let an attacker swap both files and the SHA-256 sidecar
44+
# would still verify against the malicious .spl. Sigstore signs
45+
# with a short-lived key minted from the workflow's OIDC token,
46+
# records the signature in the immutable Rekor transparency log,
47+
# and produces a .sig + .crt pair that customers verify with
48+
# `cosign verify-blob` against the `RelativisticJet/wl_manager`
49+
# workflow identity — proving the artifact came from THIS
50+
# repository's release pipeline, not a Releases-page swap.
51+
#
52+
# Verification by downstream admins:
53+
# cosign verify-blob \
54+
# --certificate wl_manager-<v>.spl.crt \
55+
# --signature wl_manager-<v>.spl.sig \
56+
# --certificate-identity-regexp 'https://github.com/RelativisticJet/wl_manager/.github/workflows/release.yml@refs/tags/.*' \
57+
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
58+
# wl_manager-<v>.spl
59+
- name: Install cosign
60+
uses: sigstore/cosign-installer@v3
61+
with:
62+
cosign-release: 'v2.4.1'
63+
64+
- name: Sign .spl with Sigstore (keyless)
65+
run: |
66+
for spl in dist/*.spl; do
67+
echo "Signing $spl..."
68+
cosign sign-blob --yes \
69+
--output-signature "${spl}.sig" \
70+
--output-certificate "${spl}.crt" \
71+
"$spl"
72+
done
73+
74+
# The SBOM is also signed so a downstream verifier can confirm
75+
# both the artifact AND its bill of materials came from THIS
76+
# workflow run (a Releases takeover would need to forge both).
77+
- name: Sign per-release SBOM with Sigstore (keyless)
78+
run: |
79+
for sbom in dist/*.cdx.json; do
80+
[ -f "$sbom" ] || continue
81+
echo "Signing $sbom..."
82+
cosign sign-blob --yes \
83+
--output-signature "${sbom}.sig" \
84+
--output-certificate "${sbom}.crt" \
85+
"$sbom"
86+
done
87+
88+
- name: Attach .spl + checksum + SBOM + signatures to release
3789
env:
3890
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3991
run: |
4092
gh release upload "${{ github.event.release.tag_name }}" \
4193
dist/*.spl \
42-
dist/*.sha256
94+
dist/*.sha256 \
95+
dist/*.cdx.json \
96+
dist/*.sig \
97+
dist/*.crt

CHANGELOG.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,80 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## Unreleased — 2026-04-29 (build 628)
5+
## Unreleased — 2026-04-29 (build 629)
6+
7+
### Round 8: residue + recurring guards + supply-chain hardening
8+
9+
#### Verified
10+
11+
- **FIM coverage live-tested**. Wrote a probe line to a `WATCH_CODE`
12+
file (`default/savedsearches.conf`) inside the container; both the
13+
~2 s stat-based watcher (`wl_fim_watch.py`, action
14+
`fim_watch_file_modified`) and the 15 s hash-sweep
15+
(`wl_fim.py`, action `fim_file_modified`) emitted audit events
16+
with the correct `monitored_path`. Confirms round 6/7 FIM
17+
additions (recovery scripts, `scripts/package.sh`, append-only
18+
`_recovery_log.jsonl`) fire end-to-end, not just look right
19+
in static review.
20+
- **Documentation cleanup**: prior commit messages and parts of
21+
CLAUDE.md called the modify-event `fim_code_modified`. The
22+
actual wire-level `action=` field is `fim_file_modified`. The
23+
inconsistency surfaced during the live FIM probe — a search for
24+
`fim_code_modified` returned no rows. The code is unchanged;
25+
only the prose was wrong. Future searches should use
26+
`fim_file_modified` (regular file changes) and
27+
`fim_watch_file_modified` (stat-watcher events).
28+
29+
#### Added
30+
31+
- **Sigstore keyless signing of the .spl + per-release SBOM** in
32+
`.github/workflows/release.yml`. Uses the workflow's OIDC token
33+
to mint short-lived ephemeral signing keys via Fulcio, records
34+
the signature in the public Rekor transparency log, and
35+
produces `<artifact>.sig` + `<artifact>.crt` for each .spl and
36+
each .cdx.json. Closes the previously-documented gap in
37+
`docs/SBOM.md` where the .sha256 + .spl shared a single channel
38+
(GitHub Releases) and a Releases takeover defeated both.
39+
Verifier command included in workflow comments.
40+
- **Quarterly `pip-audit` CI workflow**
41+
(`.github/workflows/pip-audit.yml`) — fires Jan 1 / Apr 1 / Jul
42+
1 / Oct 1 at 09:00 UTC and on `workflow_dispatch`. Fails the
43+
workflow on any vulnerability, surfacing via GitHub's existing
44+
notification settings. Origin: round 7 B4 was a one-off run;
45+
without recurrence we'd forget to re-audit.
46+
- **Per-release SBOM generation** (`scripts/generate_sbom.py`) —
47+
extracts the .spl tarball, hashes every bundled file, and emits
48+
a CycloneDX 1.5 JSON document with one `application:wl_manager`
49+
envelope and per-file `component` entries. `scripts/package.sh`
50+
now calls it as step 6/6, producing `<artifact>.cdx.json`
51+
alongside `<artifact>.spl.sha256`. Replaces the static
52+
`sbom.cdx.json` baseline (round 7 C1) with a per-release
53+
artifact that matches the .spl byte-for-byte.
54+
- **`wl_audit` long-term archival guidance** in
55+
`default/indexes.conf` — documents two options for going past
56+
the default 3-year retention (extend online vs. archive on
57+
freeze via `coldToFrozenScript`), with example config blocks
58+
and pointers to Splunk's official docs. No default changed —
59+
guidance only.
60+
- **`.append()` / `$(htmlString)` audit extension** appended to
61+
`docs/HTML_INJECTION_AUDIT.md`. 62 jQuery DOM-injection sinks
62+
beyond `.html()` audited (40 append + 3 prepend + 3 before + 1
63+
after + 5 replaceWith + 10 factory). Result: zero XSS bugs —
64+
every string-arg site already escapes user-controlled
65+
substrings. Same project-wide convention as round 7 C3 found.
66+
67+
#### Operational
68+
69+
- **Q3 2026 Splunk version-pinning audit scheduled**
70+
(`run_once_at: 2026-07-18T07:00:00Z` = 09:00 Europe/Warsaw).
71+
Remote routine `trig_01QE78KzCtSTuwFv2LjrUQqC` will re-run
72+
pip-audit, probe Splunk's supported-versions list, scan for new
73+
9.3.x CVEs, assess 10.x compat against the 7 risk areas listed
74+
in CLAUDE.md, run the pure-Python test suite, and open a PR
75+
with findings. One-shot rather than recurring because each
76+
audit's findings shape the next prompt.
77+
78+
## Released — 2026-04-29 (build 628)
679

780
### Round 7 C items: SBOM + backup/restore + .html() audit
881

appserver/static/whitelist_manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// disk cache. Splunk serves /static/@<server-hash>/... with Cache-Control:
1212
// public, max-age=31536000; without urlArgs, bumped build numbers don't force
1313
// a re-fetch and clients run stale JS until they hard-refresh.
14-
require.config({ urlArgs: "_b=628" });
14+
require.config({ urlArgs: "_b=629" });
1515
require([
1616
"jquery",
1717
"underscore",

default/app.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
[install]
77
is_configured = false
8-
build = 628
8+
build = 629
99

1010
[launcher]
1111
author = Security Engineering

default/indexes.conf

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,48 @@ thawedPath = $SPLUNK_DB/wl_audit/thaweddb
1010

1111
# Retain audit data for 3 years (adjust to your retention policy)
1212
frozenTimePeriodInSecs = 94608000
13+
14+
# ─────────────────────────────────────────────────────────────────────
15+
# Long-term archival (round 8 guidance, 2026-04-29)
16+
# ─────────────────────────────────────────────────────────────────────
17+
#
18+
# Without a `coldToFrozenScript` or `coldToFrozenDir` setting,
19+
# Splunk DELETES buckets when they pass `frozenTimePeriodInSecs`.
20+
# That's fine for the default 3-year retention, but compliance
21+
# regimes (PCI DSS 12 months online + 12 months archive, HIPAA 6
22+
# years, SOX 7 years, GDPR per local regulator) often require
23+
# longer evidentiary retention than is practical to keep online.
24+
#
25+
# Two options for going past 3 years:
26+
#
27+
# Option A — extend online retention. Simpler but uses more disk:
28+
#
29+
# [wl_audit]
30+
# # 7 years online
31+
# frozenTimePeriodInSecs = 220752000
32+
# # ~10 GB/year forecast; sized for round 7 B5 worst case
33+
# # (whitelist_view at 4 workers × 100 analysts × 50 CSVs)
34+
# maxTotalDataSizeMB = 100000
35+
#
36+
# Option B — archive frozen buckets to cold storage. Recommended
37+
# for any retention beyond 3 years:
38+
#
39+
# [wl_audit]
40+
# # 1 year online (then frozen)
41+
# frozenTimePeriodInSecs = 31536000
42+
# # Splunk runs this script on each bucket BEFORE freezing.
43+
# # Customer-supplied — copies the bucket to S3 / NAS / Glacier
44+
# # / Azure Blob and exits 0 to allow Splunk to delete the
45+
# # online copy. Splunk passes the bucket path as $1.
46+
# coldToFrozenScript = "$SPLUNK_HOME/bin/python" "$SPLUNK_HOME/etc/apps/wl_manager/bin/wl_freeze_archive.py"
47+
#
48+
# A reference `wl_freeze_archive.py` is NOT shipped because the
49+
# archive target (S3 bucket, Azure container, NFS share) is
50+
# customer-specific. Splunk's official docs cover the script
51+
# contract:
52+
# https://docs.splunk.com/Documentation/Splunk/latest/Indexer/Automatearchiving
53+
#
54+
# Whichever option you pick, document the choice in your runbook
55+
# alongside the scheduled "verify the archive is restorable"
56+
# rehearsal — an archive nobody has ever restored from is a
57+
# theoretical retention guarantee, not a real one.

docs/HTML_INJECTION_AUDIT.md

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,27 +79,56 @@ Even though no bug was found, two preventive measures landed:
7979
sites can migrate opportunistically; they don't have to migrate
8080
all at once.
8181

82+
## Extension audit — round 8 (2026-04-29)
83+
84+
Round 8 extended the same methodology to the remaining jQuery
85+
DOM-injection sinks: `.append(string)`, `.prepend(string)`,
86+
`.before(string)`, `.after(string)`, `.replaceWith(string)`, and
87+
the `$(htmlString)` factory form. Same XSS surface as `.html()`
88+
all parse HTML when given a string starting with `<`.
89+
90+
**Sweep**: 62 sites total (40 + 3 + 3 + 1 + 5 + 10). Result: **zero
91+
XSS bugs found**.
92+
93+
| Sink | Sites | Object args (safe) | String args | Bugs |
94+
|------|------:|-------------------:|------------:|-----:|
95+
| `.append()` | 40 | 32 | 8 | 0 |
96+
| `.prepend()` | 3 | 1 | 2 | 0 |
97+
| `.before()` | 3 | 1 | 2 | 0 |
98+
| `.after()` | 1 | 0 | 1 | 0 |
99+
| `.replaceWith()` | 5 | 3 | 2 | 0 |
100+
| `$('<...')` factory | 10 | 10 | 0 | 0 |
101+
102+
The string-arg sites that take user data follow the same project
103+
convention round 7 C3 documented: every user-controlled substring
104+
is `_.escape`-wrapped before concatenation. Notable patterns:
105+
106+
- `wl_modals.js`, `wl_save.js`, `wl_csv_io.js`, `wl_presence.js`,
107+
`wl_datepicker.js`, `wl_table.js` — all modal/popup builders
108+
pass already-constructed jQuery objects (`$modal`, `$bubble`,
109+
`$datePicker`), so the HTML-parsing path is bypassed entirely.
110+
- `control_panel.js:1878, 2886``replaceWith(renderLimitHistory(...))` /
111+
`replaceWith(renderAdminLimitHistory(...))` pass strings, but the
112+
builder functions escape every user-controlled field (admin name,
113+
timestamp, change values).
114+
- `wl_versions.js:96, 117, 127``<option>` tags appended to
115+
the revert dropdown; `_.escape` per filename and display string.
116+
- `wl_approval_ui.js:430, 572, 574` — approval-bar and addition-
117+
preview HTML; `_.escape` on every header, cell, action_type,
118+
analyst, reason, request_id.
119+
82120
## What this audit does NOT cover
83121

84-
- jQuery `.append(string)` / `.prepend(string)` / `.before(string)`
85-
/ `.after(string)` — when called with a string argument, these
86-
also parse HTML and have the same XSS surface as `.html()`. Not
87-
audited in this round; future work.
88-
- `$(htmlString)` — the jQuery factory parses HTML when the
89-
argument starts with `<`. Not audited.
90122
- DOM `innerHTML` / `outerHTML` — pure DOM API, search-and-audit
91123
separately. Quick grep of the codebase: zero hits in production
92124
code.
93125
- `eval`, `Function(string)`, `setTimeout(string, ...)` — these
94126
would be a different class of bug. Quick grep: zero hits in
95127
production code.
96128

97-
A follow-up round can extend the same methodology to `.append()`
98-
et al. The rationale for stopping at `.html()` here is that it
99-
has the highest concentration of user-data-into-DOM call sites and
100-
the audit returned a clean result; the cost-to-coverage of going
101-
deeper drops sharply once we have confidence the discipline is
102-
consistent.
129+
The methodology has now covered every realistic XSS sink in
130+
jQuery + DOM that this codebase actually uses. Going deeper hits
131+
diminishing returns.
103132

104133
## Re-audit triggers
105134

0 commit comments

Comments
 (0)