Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Dependency updates.
#
# Every `uses:` in this repository is pinned to a commit SHA, which is the only
# pin a tag cannot be repointed around — but a SHA never expires on its own, so
# without a bot the pins silently rot at whatever was current the day they were
# written. Dependabot rewrites the SHA and its trailing `# vN` comment together.
version: 2

updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
# One PR per week for the whole set. Each action is still reviewed on its
# own diff line; separate PRs would only multiply the CI runs.
actions:
patterns: ["*"]

- package-ecosystem: pip
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
python:
patterns: ["*"]
3 changes: 2 additions & 1 deletion .github/workflows/acs-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ jobs:
- name: Install project
run: |
python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pip install --require-hashes -r requirements-ci.txt
.venv/bin/python -m pip install -e . --no-deps

- name: Check current public ACS contract
run: >-
Expand Down
71 changes: 66 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,21 @@ jobs:
python-version: "3.14"
cache: pip

# `venv` and `pip` ship with Python, so this is the exact command a
# contributor runs locally.
# Dependencies come from a hash-pinned lock, so a yanked-and-replaced
# release on PyPI cannot change what this job runs against a live server
# and the credentials in its env. The project installs separately with
# --no-deps: nothing resolves outside the lock. The `gate` and `test`
# jobs above deliberately keep resolving `.[dev]` live — they carry no
# secrets, and their purpose is to exercise the install path
# CONTRIBUTING.md documents. A future job that touches a secret must be
# pinned regardless of whether it also needs to test that live path —
# don't let the two goals get conflated into "pin if it happens to be
# live-install-focused."
- name: Install project
run: |
python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pip install --require-hashes -r requirements-ci.txt
.venv/bin/python -m pip install -e . --no-deps

- name: Stage server fixtures
env:
Expand Down Expand Up @@ -276,6 +285,58 @@ jobs:
enterprise-*.xml
if-no-files-found: ignore

# Fuzz the credential stripper. `safe_target` is what keeps a password in
# SPLUNK_URL out of prompts, JSON metadata, and the audit log, and it is
# handed the URL exactly as typed — before anything validates that it parses.
# A short run per PR is enough to catch a regression in the shapes that
# example-based tests do not think to write down.
#
# Linux-only, and separate from the test matrix: atheris publishes manylinux
# x86_64 wheels only, so it must never join pyproject.toml's `dev` extra.
fuzz:
name: Fuzz / Credential stripper
needs: [gate, test, changes]
if: >-
${{
!cancelled() && needs.gate.result == 'success' &&
needs.test.result == 'success' &&
(github.event_name != 'pull_request' ||
needs.changes.outputs.code == 'true')
}}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
cache: pip

- name: Install project
run: |
python -m venv .venv
.venv/bin/python -m pip install --require-hashes -r requirements-fuzz.txt
.venv/bin/python -m pip install -e . --no-deps

# Bounded by wall clock rather than iteration count so the job cost stays
# predictable as the corpus grows.
- name: Fuzz safe_target
run: .venv/bin/python tests/fuzz/fuzz_redact.py -max_total_time=60 -print_final_stats=1

- name: Upload crash corpus
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fuzz-crash-${{ github.run_id }}
path: crash-*
if-no-files-found: ignore

# ============================================================================
# MERGE GATE — the only check branch protection requires.
# `name:` MUST stay "Merge Gate": required_status_checks matches the context
Expand All @@ -285,7 +346,7 @@ jobs:
# ============================================================================
merge-gate:
name: Merge Gate
needs: [dependency-review, workflow-security, gate, test, changes, enterprise]
needs: [dependency-review, workflow-security, gate, test, changes, enterprise, fuzz]
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-latest
permissions:
Expand All @@ -298,5 +359,5 @@ jobs:
# a skip is a legitimate outcome and must not fail the gate. `gate` and
# `test` are deliberately absent: they run on every event and must
# succeed.
allowed-skips: dependency-review, workflow-security, changes, enterprise
allowed-skips: dependency-review, workflow-security, changes, enterprise, fuzz
jobs: ${{ toJSON(needs) }}
3 changes: 2 additions & 1 deletion .github/workflows/cloud-read.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ jobs:
if: steps.stack.outputs.ready == 'true'
run: |
python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pip install --require-hashes -r requirements-ci.txt
.venv/bin/python -m pip install -e . --no-deps

- name: Cloud reads (every catalogued read command)
if: steps.stack.outputs.ready == 'true'
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,27 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`pre-commit` directly. The install path users are told to use is now the one
that is tested on every pull request, and the project depends on no build tool
beyond what ships with Python. Workflow-security analysis stays centralized.
- Install from a hash-pinned lock in the three jobs that run against a live
server or Splunk's public API, so a replaced release on PyPI cannot change
what they run. The lint, type, and test jobs still resolve `.[dev]` live,
because their purpose is to exercise the documented install path. The
contributor install is unchanged; see CONTRIBUTING.md for regenerating the
lock after a dependency edit.
- Add weekly dependency updates, covering both Python packages and the
commit-pinned GitHub Actions.
- Fuzz `safe_target`, the function that strips credentials out of a Splunk URL
before it is printed. It runs on every pull request that touches code.

### Fixed

- Redact a credential in a Splunk URL that earlier releases echoed back. Any
URL the redactor could not rebuild was returned as it stood, so a truncated
authority, a missing scheme, a credential following a path separator, or one
in a `?token=` query or `#` fragment all reached the printed target intact,
and an unterminated IPv6 literal raised with the value in the traceback.
Redaction now fails closed: a target it cannot rebuild is replaced rather
than repeated, and the query and fragment are dropped on every path. A target
carrying no credential still prints in full, so error messages stay readable.
- Capture stderr separately in the two tests that assert a secret does not reach
it. Click below 8.2 folds stderr into stdout unless asked not to, and 8.2
removed the parameter that asks, so both assertions raised on the 3.9
Expand Down
25 changes: 21 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,27 @@ Continuous integration runs the same hook set in one command, which you can too:
pre-commit run --all-files
```

Four more test groups run against a live server, a live Splunk Cloud stack, or
Splunk's published API description. Each is off until you switch it on.
[tests/TESTING.md](./tests/TESTING.md) gives every group its exact variables,
its exact command, and the container setup the destructive write lane needs.
Five more test groups run against a live server, a live Splunk Cloud stack,
Splunk's published API description, or a fuzzer. Each is off until you switch it
on. [tests/TESTING.md](./tests/TESTING.md) gives every group its exact
variables, its exact command, and the container setup the destructive write lane
needs.

## If you change a dependency

Install with `pip install -e ".[dev]"` as above — that has not changed. But CI
installs from `requirements-ci.txt`, a hash-pinned lock, so that a replaced
release on PyPI cannot change what CI runs. After editing dependencies in
`pyproject.toml`, regenerate it, or CI keeps resolving the old versions:

```bash
uv pip compile requirements-ci.in --generate-hashes \
--no-emit-package vct-splunk-cli --python-version 3.14 -o requirements-ci.txt
```

`requirements-fuzz.txt` is the same idea for the fuzz job; its header carries
its own command. Both `.in` files list `.`, so version bounds stay declared once
in `pyproject.toml`.

## Project layout

Expand Down
3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
packages = [
pkgs.python314
pkgs.ruff
# Regenerates the hash-pinned CI locks; not needed to develop or
# test. See the header of requirements-ci.in for the command.
pkgs.uv
];
shellHook = ''
echo "vct-splunk-cli dev shell. First run:"
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ select = [
# Check against the floor, not the interpreter that happens to be in .venv.
pythonVersion = "3.9"
include = ["src", "tests"]
# atheris publishes manylinux x86_64 wheels only, so it is absent from every
# development environment that is not Linux and cannot be resolved here. The
# fuzz job on Linux is where that file is exercised.
exclude = ["tests/fuzz"]
extraPaths = ["src"]
venvPath = "."
venv = ".venv"
Expand Down
9 changes: 9 additions & 0 deletions requirements-ci.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Source for requirements-ci.txt — the exact set the three integration workflows
# install. `.` defers to pyproject.toml, so dependency bounds stay declared once.
#
# Regenerate after changing pyproject.toml or this file:
# uv pip compile requirements-ci.in --generate-hashes \
# --no-emit-package vct-splunk-cli --python-version 3.14 \
# -o requirements-ci.txt
.
pytest
57 changes: 57 additions & 0 deletions requirements-ci.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements-ci.in --generate-hashes --no-emit-package vct-splunk-cli --python-version 3.14 -o requirements-ci.txt
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via httpx
certifi==2026.7.22 \
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
# via
# httpcore
# httpx
click==8.4.2 \
--hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \
--hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76
# via vct-splunk-cli
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via vct-splunk-cli
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
# via
# anyio
# httpx
iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
# via pytest
packaging==26.3 \
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
# via pytest
pluggy==1.6.0 \
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
# via pytest
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
# via pytest
pytest==9.1.1 \
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
# via -r requirements-ci.in

# The following packages were excluded from the output:
# vct-splunk-cli
12 changes: 12 additions & 0 deletions requirements-fuzz.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Source for requirements-fuzz.txt — the fuzz job only.
#
# atheris publishes manylinux x86_64 wheels only, so this set is resolved for
# Linux and installed nowhere else. Keeping it out of pyproject.toml's `dev`
# extra is what lets `pip install -e ".[dev]"` keep working on macOS.
#
# Regenerate after changing pyproject.toml or this file:
# uv pip compile requirements-fuzz.in --generate-hashes \
# --no-emit-package vct-splunk-cli --python-version 3.14 \
# --python-platform x86_64-unknown-linux-gnu -o requirements-fuzz.txt
.
atheris
42 changes: 42 additions & 0 deletions requirements-fuzz.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements-fuzz.in --generate-hashes --no-emit-package vct-splunk-cli --python-version 3.14 --python-platform x86_64-unknown-linux-gnu -o requirements-fuzz.txt
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via httpx
atheris==3.1.0 \
--hash=sha256:315a0b5c819852b1ffe1ca72efc389c7724881f2c33e4aacb8c6bcec49bd5011 \
--hash=sha256:ec5e11f21a4c197fe91f7aea2b2de88e623c73a21fc07b105ac6329a1588457b \
--hash=sha256:f8a9f51ce8369026e8eb7b7174835e8c4c85a1a6db5d9add36c15100779d2a39
# via -r requirements-fuzz.in
certifi==2026.7.22 \
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
# via
# httpcore
# httpx
click==8.4.2 \
--hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \
--hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76
# via vct-splunk-cli
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via vct-splunk-cli
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
# via
# anyio
# httpx

# The following packages were excluded from the output:
# vct-splunk-cli
Loading