Features planned for future releases of sendit. Contributions are welcome — open an issue to discuss before starting work on a large item.
Completed
- v0.1.0 — Initial release ✓
- v0.2.0 — Result export ✓
- v0.3.0 — Probe command ✓
- v0.4.0 — Config hot-reload ✓
- v0.5.0 — Security CI ✓
- v0.6.0 — Documentation site ✓
- v0.7.0 — Container support ✓
- v0.8.0 — Observability improvements ✓
- v0.9.0 — Probe WebSocket ✓
- v0.10.0 — Distribution ✓
- v0.10.4 — Repository security hardening ✓
- v0.10.5 — macOS code signing and notarization ✓
- v0.10.6 — Packet capture ✓
- v0.11.0 — Config generator ✓
- v0.11.1 — Arch Linux package ✓
- v0.12.0 — OSSF Scorecard: Token-Permissions ✓
- v0.12.1 — OSSF Scorecard: Pinned-Dependencies ✓
- v0.12.2 — OSSF Scorecard: Signed-Releases ✓
- v0.12.3 — OSSF Scorecard: Branch-Protection ✓
- v0.12.4 — OSSF Scorecard: CII Best Practices ✓
- v0.12.5 — OSSF Scorecard: Fuzzing ✓
- v0.12.6 — OpenSSF Best Practices: gap audit ✓
- v0.13.0 — Changelog and release notes ✓
- v0.13.1 — Test coverage ✓
- v0.13.2 — Benchmark suite ✓
- v0.13.3 — Dependency audit ✓
- v0.13.4 — Table of contents ✓
- v0.11.2 — AUR package ✓
- v0.14.0 — Safari bookmarks + browser history tests ✓
- v0.14.1 — Burst pacing mode +
--durationflag ✓ - v0.14.2 — AUR latest sync ✓
- v0.15.0 — Test coverage improvement ✓
- v0.15.1 — Integration test suite expansion ✓
- v0.15.2 — Codecov Test Analytics ✓
- v0.15.3 — Docs audit + fuzz CI fix ✓
- v1.0.0 — TUI + stable API ✓
- v1.1.0 — gRPC driver ✓
- v1.2.0 — Auth support ✓
- v1.6.0 — SFTP driver ✓
Planned
Research
- Non-standard traffic driver
- Aggressive / burst pacing mode ✓ (promoted to v0.14.1)
- Browser history and bookmarks harvesting ✓ (shipped in v0.11.0 / v0.14.0)
- Live packet capture
- Multi-browser support (post-v1.0.0)
- Four driver types: HTTP, headless browser (chromedp), DNS, WebSocket
- Three pacing modes:
human(random delay),rate_limited(token bucket),scheduled(cron windows) - Weighted target selection using the Vose alias method (O(1) picks)
- Prometheus metrics with per-domain rate limiting and decorrelated jitter backoff
- CPU and memory resource gates that pause dispatch when thresholds are exceeded
--dry-runflag to preview effective config before sending traffic- Integration test suite covering the full dispatch pipeline
Write request results to a file for offline analysis, complementing the Prometheus scrape endpoint.
- New
outputconfig section:file,format(jsonl|csv),append(bool) - A dedicated writer goroutine consumes results non-blocking to the dispatch loop
- Truncates or appends on startup based on the
appendflag
A sendit probe <target> subcommand for interactively testing a single HTTP or DNS endpoint
in a loop — no config file needed. Works like ping for web targets.
- Auto-detects type from URL scheme (
https://→ http, bare hostname → dns) --type,--interval,--timeout,--resolver,--record-typeflags- Prints one line per request with status, latency, and bytes (HTTP) or rcode (DNS)
- Prints a summary (sent, ok, errors, min/avg/max latency) on Ctrl-C
$ sendit probe https://example.com
Probing https://example.com (http) — Ctrl-C to stop
200 142ms 1.2 KB
200 38ms 1.2 KB
^C
--- https://example.com ---
2 sent, 2 ok, 0 error(s)
min/avg/max latency: 38ms / 90ms / 142ms
Reload configuration on SIGHUP without restarting the process or dropping in-flight requests.
- Targets and weights swapped atomically via the existing
task.Selector - Pacing, rate-limit, and backoff registries updated in-place where possible
- Logs a diff of what changed (added/removed targets, updated limits)
Automated security scanning integrated into every PR and a weekly scheduled run.
govulncheck— scans all dependencies against the Go vulnerability database (vuln.go.dev); fails the build on any known CVEgosec— SAST linter added to golangci-lint; checks for insecure patterns in source code (weak crypto, command injection, file permission issues, etc.)- CodeQL — GitHub's semantic analysis engine; results surface in the repository Security tab
- Dependabot — weekly automated PRs for stale Go module and GitHub Actions dependencies
Small improvements tracked as GitHub issues that will ship as patch releases before the next minor version.
- WebSocket driver migration ✓ — migrate
internal/driver/websocket.gofrom the deprecatednhooyr.io/websocketto its maintained forkgithub.com/coder/websocket(#23) sendit reloadcommand ✓ — sendSIGHUPto a running instance via its PID file, making hot-reload a first-class CLI operation consistent withsendit stop(#26)
Public reference documentation hosted on GitHub Pages.
- Built with Hugo, source under
docs/ - Pages: getting started, configuration reference, pacing modes, drivers, metrics, CLI reference
- Deployed automatically on every push to
mainvia GitHub Actions
Package sendit as a Docker image for portability and scheduled runs in CI or on a server.
- Multi-stage
Dockerfile:golang:1.24-alpinebuilder →alpineruntime (files underdocker/) docker-compose.ymlwith optional Prometheus + Grafana sidecars via--profile observability- Config mounted as a volume so the image stays generic
--foregroundset by default in the entrypoint (PID files are not useful inside a container)/healthzendpoint on the metrics port for container liveness checks
Better visibility into per-target behaviour from Prometheus metrics.
- Add a
domainlabel tosendit_requests_total,sendit_errors_total, andsendit_request_duration_secondsso individual targets can be distinguished in dashboards - Note: this is a breaking change to existing metric label sets — update any dashboards or alerts accordingly
Complete driver coverage in the probe tool.
- Extend
sendit probeto supportwss://targets; connects, optionally sends a message, waits for a reply, and prints latency per round-trip
Make sendit easy to install without building from source across all supported platforms.
- Homebrew tap —
brew install lewta/tap/sendit; newlewta/homebrew-taprepo auto-updated by GoReleaser on each release via thebrews:config section; formula bundles shell completions for bash, zsh, and fish - Linux packages —
.deband.rpmartifacts added to each release via GoReleasernfpms:; covers apt users (Debian, Ubuntu) and yum/dnf users (Fedora, RHEL, CentOS); targets linux/amd64 and linux/arm64; bundles shell completions and a man page - Scoop bucket —
scoop install lewta/sendit; newlewta/scoop-bucketrepo auto-updated by GoReleaser via thescoops:config section; provides Windows package manager parity with Homebrew - Shell completion install docs — update
docs/content/docs/cli.mdwith per-method install instructions: Homebrew (automatic via formula),.deb/.rpm(bundled), and binary download (manualsourcecommands for bash/zsh/fish)
Establish a clear vulnerability disclosure process and harden CI/CD supply-chain security.
SECURITY.md— security policy file defining supported versions, the reporting process (GitHub private advisory), response timelines (48 h acknowledgement, 7-day resolution target), and coordinated disclosure policy- Private vulnerability reporting — enable GitHub's private vulnerability reporting so reporters can submit CVEs without opening a public issue
- Dependabot security updates — enable automated security-fix PRs (distinct from the version-update PRs already in place)
- Branch ruleset hardening — set
dismiss_stale_reviews_on_push: trueso post-approval pushes require re-review - OSSF Scorecard — add
scorecard.ymlGitHub Actions workflow; runs weekly and on every push tomain; publishes results to the GitHub Security tab as SARIF - Docs — Security page — add
docs/content/docs/security.mdsummarising the policy, supported versions, and how to report - Docs —
security.txt— adddocs/static/.well-known/security.txt(RFC 9116) so automated scanners and researchers can discover the disclosure contact and policy URL
Sign and notarize the darwin binaries so macOS Gatekeeper accepts them without any user intervention. Fixes #95.
- GoReleaser
notarizeblock — useanchore/quill(cross-platform; runs on the existingubuntu-latestrunner, no macOS runner needed) to sign darwin/amd64 and darwin/arm64 binaries with a Developer ID Application certificate and submit them to Apple's notarization service via the App Store Connect API before archiving - GitHub secrets —
MACOS_SIGN_P12(base64.p12),MACOS_SIGN_PASSWORD,NOTARIZE_KEY(base64.p8),NOTARIZE_KEY_ID,NOTARIZE_ISSUER_ID; all sourced from the lewta Apple Developer account - Remove caveats workaround — once notarization is in place, remove the temporary
caveatsstanza added to the Homebrew cask in v0.10.3
A sendit generate subcommand that produces a ready-to-use config.yaml from a targets file or a seed URL, reducing the time-to-first-traffic for new users.
- From a targets file — parse an existing
targets_file(url + type + optional weight, one per line) and emit a fullconfig.yamlwith sensible defaults for pacing, limits, backoff, and per-target driver settings - From a seed URL (
--crawl) — for HTTP targets, optionally crawl the seed domain up to a configurable depth/page limit, discover in-domain links, and add each unique path as a weightedhttptarget; respectsrobots.txtby default (--ignore-robotsto override) - From browser history (
--from-history) — read the local browser history database and emit all visited HTTP/HTTPS URLs as weightedhttptargets; weight derived from visit count so frequently visited pages appear more often in traffic (see Research item below) - From browser bookmarks (
--from-bookmarks) — read the local browser bookmarks file and emit bookmarked HTTP/HTTPS URLs as equally-weightedhttptargets - Output — writes to stdout by default;
--output <file>writes to a file, prompting before overwriting - Flags:
--targets-file <path>— generate from an existing targets file--url <url>— seed URL for crawl-based generation (implies--crawl)--crawl— enable in-domain page discovery for HTTP targets--depth <n>— maximum crawl depth (default:2)--max-pages <n>— maximum number of pages to discover (default:50)--ignore-robots— skiprobots.txtenforcement during crawl--from-history <browser>— harvest visited URLs from local browser history (chrome|firefox|safari)--from-bookmarks <browser>— harvest bookmarked URLs from local browser bookmarks (chrome|firefox|safari)--history-limit <n>— cap the number of URLs imported from history (default:100, ordered by visit count descending)--output <file>— write config to a file instead of stdout
Example:
# From a targets file
sendit generate --targets-file config/targets.txt > config/generated.yaml
# From a seed URL with crawling
sendit generate --url https://example.com --crawl --depth 2 --output config/generated.yaml
# From Chrome history (top 50 most-visited pages)
sendit generate --from-history chrome --history-limit 50 --output config/generated.yaml
# From Firefox bookmarks
sendit generate --from-bookmarks firefox --output config/generated.yamlDocumentation deliverables (required as part of the same release):
- CLI help —
Use,Short, andLongdescriptions on thegeneratecommand and all flags, consistent with the style ofprobeandpinch README.md— addsendit generateto the CLI commands usage block and command table; add a Generate section with both usage modes and example output, alongside the existing Probe and Pinch sectionsdocs/content/docs/cli.md— addgenerateto the commands block and table; add agenerateflags section with both modes, flag reference, and annotated example outputdocs/content/docs/getting-started.md— add a "Generate a config from a URL" subsection under the quick-start flow so new users discover the crawl mode as the fastest path to a working config
Make sendit installable as a native Arch Linux package so Arch and Arch-based users (e.g. Omarchy) can install it from the releases page without building from source.
- GoReleaser
nfpms: archlinux— addarchlinuxto thenfpms: formatslist; GoReleaser produces a.pkg.tar.zstartifact on every release - Shell completions — zsh completion installed to
/usr/share/zsh/site-functions/_sendit(Arch convention; deb/rpm continue to use/usr/share/zsh/vendor-completions/) - Docs — update
README.mdanddocs/content/docs/getting-started.mdwith thepacman -Uinstall command for Arch / Omarchy users
# Arch Linux / Omarchy (and other Arch-based distros)
sudo pacman -U sendit_<version>_linux_amd64.pkg.tar.zstMake sendit installable via Arch User Repository helpers so Arch Linux and Arch-based users (e.g. Omarchy) can install with a single command:
yay -S sendit # or: paru -S senditPrerequisites (manual setup before implementation):
- Register
senditas an AUR package at aur.archlinux.org - Generate a dedicated SSH key pair:
ssh-keygen -t ed25519 -C "sendit-aur" - Add the public key to your AUR account profile
- Add the private key as the
AUR_SSH_KEYGitHub Actions secret
Implementation:
- Add
aurs:block to.goreleaser.yamlpointing atssh://aur@aur.archlinux.org/sendit.git; GoReleaser generates and pushes aPKGBUILDon every release that downloads the source tarball and verifies its SHA-256 againstchecksums.txt— no binary distribution needed - Add
AUR_SSH_KEY: "placeholder"to the env blocks ingoreleaser-checkandgoreleaser-snapshotCI jobs so template evaluation passes on PRs without the real secret - Update
README.mdanddocs/content/docs/getting-started.mdto documentyay/paruinstall alongside the.pkg.tar.zstdownload option added in v0.11.1
Add an explicit opt-in burst pacing mode for internal infrastructure testing and controlled load experiments. sendit stays polite by default — burst requires being asked nicely.
Design principles:
mode: burstis set in the config file, not a runtime flag — it is a deliberate configuration choice, not something that can be accidentally triggered--durationis required whenmode: burst; the engine refuses to start a burst run without a time bound; this is the primary safety gate that prevents open-ended hammering- The resource gate (
cpu_threshold_pct,memory_threshold_mb) still applies — the local machine always protects itself - Backoff still engages on repeated errors — burst mode does not disable error handling
- Clearly documented as intended for internal or owned infrastructure; pointing burst at external targets you do not control is out of scope and discouraged
Implementation:
mode: burstin thepacing:config block — fires requests as fast as worker slots allow with no inter-request delay;min_delay_ms/max_delay_ms/requests_per_minuteare ignoredramp_up_s— optional field in thepacing:block; linearly increases active workers from 1 tomax_workersover the specified number of seconds; applies toburstmode only; prevents a cold-start spike against the target--duration <duration>onsendit start— auto-stops the engine after the specified wall-clock time (e.g.--duration 5m,--duration 30s); required whenmode: burst, optional otherwise; on expiry the engine performs a graceful shutdown (drains in-flight requests) identical to SIGTERM- Config validation —
config.Loadreturns an error ifmode: burstand--durationwas not passed; enforced at startup, not silently defaulted - README key properties — update "Never bursts aggressively" to reflect the opt-in design
- Docs — burst mode documented in
docs/content/docs/pacing.mdwith an explicit "internal use" callout;--durationflag documented indocs/content/docs/cli.md
Distribution-only patch. The initial AUR publication in v0.11.2 was out-of-sequence
(version number lower than the current latest), leaving the AUR pointing at old
binaries. This release updates the AUR PKGBUILD to the current latest so
yay -S sendit installs up-to-date code.
Harden GitHub Actions workflow token permissions to follow the principle of least privilege. Fixes the Token-Permissions check (currently 0/10).
ci.yml— addedpermissions: read-allat the top level; the workflow has no write needsrelease.yml— replaced top-levelcontents: writewithpermissions: read-alland scopedcontents: writeto thereleasejob onlydocs.yml— movedpages: writeandid-token: writefrom the top level to thedeployjob only;buildjob only needscontents: read
Pin all GitHub Actions dependencies to their full commit SHA and all Docker base images to their digest. Fixes the Pinned-Dependencies check (currently 0/10).
- GitHub Actions — replaced all
uses: action/name@vX.Y.Zreferences across all five workflow files withuses: action/name@<sha> # vX.Y.Z; also aligneddocs.ymlfrom checkout@v4/setup-go@v5 to v6 - Docker images — pinned
golang:1.24-alpineandalpine:3.21indocker/Dockerfileto their@sha256:…digests - Dependabot — already configured for
github-actionsweekly updates; will keep pinned SHAs current automatically
Attach SLSA provenance attestations to every release artifact so consumers can verify the build was produced by this repository's CI without tampering. Fixes the Signed-Releases check (currently 0/10).
actions/attest-build-provenance— added as the final step of thereleasejob; generates GitHub-native SLSA provenance for all archives (.tar.gz,.zip), packages (.deb,.rpm,.pkg.tar.zst), andchecksums.txt; attestations are stored in GitHub's attestation store and verifiable withgh attestation verifyrelease.ymlpermissions — addedid-token: writeandattestations: writeto thereleasejob- Docs — added "Build provenance" section to
docs/content/docs/security.mdwith verification instructions
Raise the Branch-Protection check by adding required status checks to the baseline-branch-rule ruleset, and bump all stale GitHub Actions and Go module dependencies.
- Required status checks — added
lintandtestCI jobs as required checks so PRs cannot be merged until both pass - Admin bypass preserved — the
RepositoryRole/Adminbypass actor is intentionally retained while the project is single-maintainer; can be removed when a second maintainer is added - Dependency updates — bumped
golang.org/x/netto 0.52.0; updatedactions/upload-artifactto v7,actions/create-github-app-tokento v3,ossf/scorecard-actionto 2.4.3,github/codeql-actionto v4, andactions/attest-build-provenanceto v4 (all SHA-pinned)
Integrate fuzz testing to catch parser and input-handling bugs that unit tests miss. Fixes the Fuzzing check (currently 0/10).
The Scorecard check accepts native Go fuzz functions (func FuzzXxx(f *testing.F)), which require no external service — just go test -fuzz.
internal/config—FuzzLoad: feeds arbitrary YAML bytes through the config loader via a temp file; catches panics and unexpected parse errors on malformed inputinternal/task—FuzzSelector: fuzzes the Vose alias selector with arbitrary-length weight slices; validates O(1) pick invariants under edge-case inputs (empty slice, zero weights, single element, skewed distributions)internal/ratelimit—FuzzClassifyError+FuzzClassifyStatusCode: fuzz both classifiers across all possible inputs; validates every result maps to a definedErrorClassinternal/pcap—FuzzWriteRecord: fuzzes the PCAP record writer with arbitrary result fields (URL, type, status, duration, bytes) including oversized payloads that exercise thesnapLentruncation pathfuzzCI job — runs each target with-fuzztime=30son every PR
Register the project on the OpenSSF Best Practices platform and link the badge. Fixes the CII-Best-Practices check (currently 0/10).
- Register — project registered at bestpractices.coreinfrastructure.org/projects/12213
- Badge — OpenSSF Best Practices badge added to
README.mdalongside the existing OSSF Scorecard badge
Worked through all passing-level criteria on the Best Practices platform to bring the badge from its initial state to passing (99%).
- Basics — all 13 criteria answered Met; evidence URLs linked for description, licence, CONTRIBUTING.md, and docs site
- Change control — all 9 criteria answered Met; release notes and CVE policy evidenced via CHANGELOG.md and GitHub Releases
- Reporting — all 8 criteria answered Met; SECURITY.md, private advisory, and 14-day response policy evidenced
- Quality — all 13 criteria answered Met;
test_mostevidenced via Codecov (v0.13.1); test policy evidenced via CONTRIBUTING.md - Security — all criteria answered Met or N/A; crypto delegated to stdlib TLS, SLSA provenance evidences delivery integrity, govulncheck + Dependabot evidence vulnerability management
- Analysis — all 8 criteria answered Met or N/A; golangci-lint/CodeQL for static analysis, fuzz tests + race detector for dynamic analysis
Establish a proper changelog and add authored release notes to every GitHub release — past and future.
CHANGELOG.md— human-authored changelog in Keep a Changelog format covering all releases from v0.1.0 to v0.12.5; CVE note policy documented in the header- Retroactive release notes — all 33 GitHub releases (v0.1.0–v0.12.5) updated with authored descriptions via
gh release edit - GoReleaser changelog groups —
changelog:block in.goreleaser.yamlnow groups future release notes by type: New features, Bug fixes, Security, CI/build/dependencies
Surface test coverage metrics so regressions are visible in CI and PRs.
- Codecov integration —
go test -coverprofile=coverage.txt -covermode=atomicin thetestCI job uploads to codecov.io viacodecov/codecov-action@v5.5.3(SHA-pinned); Codecov badge added toREADME.md - Coverage gate —
codecov.ymlconfigures a project gate (≤2% drop vs base branch) and a patch gate (≥50% coverage on new code per PR)
Add Go benchmarks for the hot paths in the dispatch loop so performance regressions are caught before they reach main.
internal/task—BenchmarkSelectorPickacross 1, 10, and 100 targets; confirms O(1) Vose alias behaviour (~28–34 ns/op, zero allocs)internal/ratelimit—BenchmarkClassifyStatusCode(~6 ns/op),BenchmarkClassifyError(~8 ns/op),BenchmarkRegistryWait(~100 ns/op); all zero allocsinternal/engine—BenchmarkDispatchwith a no-op driver stub (~1 µs/op, 3 allocs); covers backoff check, rate-limit check, and metrics recording- CI —
benchjob runsgo test -bench=. -benchmem -run='^$'on every PR and storesbench.txtas abench-resultsartifact
Review and tighten the dependency tree before committing to a stable v1.0.0 API.
go mod tidy— module graph confirmed clean; no unused indirect dependencies- Licence audit — all 12 direct dependencies carry permissive licences (MIT, ISC, BSD-3-Clause, Apache-2.0); all compatible with the project's MIT licence
- Alternatives review —
x/net/html(no stdlib HTML parser),x/time/rate(no stdlib token bucket),viper(env-overlay config complexity),zerolog(zero-allocation overlog/slog) all retained as justified; all other deps are the only practical choice for their driver or feature docs/content/docs/dependencies.md— published page listing all 12 direct deps with purpose, licence, and alternatives rationaledocs/content/docs/ossf.md— published OpenSSF Best Practices evidence page (supersedes local working document)
Complete the browser input sources introduced in v0.11.0: add Safari bookmarks support and add fixture-based unit tests for all SQLite and plist reading paths.
- Safari bookmarks —
sendit generate --from-bookmarks safarinow reads~/Library/Safari/Bookmarks.plistusinghowett.net/plist(MIT); handles both binary and XML plist formats; recursively extracts HTTP/HTTPS URLs from nested bookmark folders; non-http schemes (e.g.reading-list://) are silently skipped; macOS-only (errors clearly on Linux) - Fixture-based tests — added unit tests for all SQLite-backed paths using
in-process databases created with
modernc.org/sqlite:- Chrome history:
historyFromSQLitewith aurlstable fixture; verifies URL filtering, visit-count weight capping (max 10), and--history-limit - Firefox bookmarks:
firefoxBookmarkswith amoz_places + moz_bookmarksfixture; verifiesJOINquery and non-http exclusion - Safari bookmarks:
safariBookmarkswith an XML plist fixture; verifies recursive folder descent, URL filtering, and weight assignment
- Chrome history:
- Research item closed — "Browser history and bookmarks harvesting" research is complete; core feature shipped in v0.11.0, Safari bookmarks completed here
Add a table of contents to the four main project documents so readers can navigate long files without scrolling.
README.md— TOC covering all 15 top-level sections using GitHub-compatible anchor linksROADMAP.md— TOC listing every milestone (completed, planned, research) with anchor linksCONTRIBUTING.md— TOC covering all 10 contribution workflow sectionsCODE_OF_CONDUCT.md— TOC covering all 7 main sections
Raise overall test coverage from its current 62.1% toward ~75% before the v1.0.0 stability commitment. The audit identified three categories of uncovered code: intentionally untestable (needs real Chrome, live network, OS process), structurally hard (engine dispatch loop), and straightforwardly testable but missing tests. This milestone targets the third category and as much of the second as is practical.
Current per-package coverage (baseline):
| Package | Coverage | Primary gap |
|---|---|---|
cmd/sendit (main) |
48.9% | probe*, pinch*, printDryRun, validateCmd all 0% |
cmd/sendit (generate) |
~70% | chromeBookmarks, path-resolution functions all 0% |
internal/engine |
55.7% | Run, dispatch 0%; Start 45%; UpdatePacing 56% |
internal/metrics |
44.1% | New, ServeHTTP both 0% |
internal/driver |
64.2% | browser.Execute 0% — intentionally skipped (needs Chrome) |
internal/ratelimit |
97.6% | — |
internal/task |
97.6% | — |
internal/resource |
100% | — |
Deliverables:
- Pure helper functions — unit tests for all zero-coverage pure functions in
cmd/sendit/main.go:probeRcodeLabel,probeFormatBytes,probeSummary,detectProbeType,pinchSummary,isConnRefused,printDryRun; these have no external dependencies and are straightforwardly table-driven - Chrome bookmarks — fixture-based tests for
chromeBookmarksandwalkChromeNodeusing a synthetic ChromeBookmarksJSON file; mirrors the Firefox and Safari fixture tests added in v0.14.0 - Browser path resolution — tests for
chromePath,firefoxPath,firefoxDefaultProfile,firefoxFallbackProfileusing temp directories; validates OS-specific path logic without touching the real filesystem historyDBInfo— expand SQLite fixture tests to cover the 11% currently missed (error paths and alternate schema branches)- Metrics — tests for
metrics.New(Prometheus registry initialisation) andmetrics.ServeHTTP(/metricsand/healthzendpoints) usinghttptest - Engine dispatch integration — a short-lived integration test that runs
engine.Runwith a stub no-op driver and a 100 ms timeout; exercisesdispatchand the full pipeline (scheduler → resource gate → pool → driver); kept in a separate_integration_test.gofile with a build tag so it does not run in the unit-test path scheduler.UpdatePacingandscheduler.Start— targeted tests for the remaining uncovered branches (mode switches, cron window lifecycle)validateCmd— extend existing tests to cover the uncovered flag/path branches
Intentionally not targeted (documented as such):
browser.Execute— requires a real Chrome binary; skip annotation already in place indriver_test.go; noted in a// coverage: intentionally skippedcommentmain()entry point,initLogger,writePID— OS-level side effects; not unit-testableprobeWS,pinchTCP,pinchUDP— require live network connections; out of scope for unit tests; candidate for a future integration test suite
The engine integration test infrastructure already exists
(internal/engine/integration_test.go, //go:build integration, 7 tests, CI job).
This milestone widens its scope, fills the missing scenarios, and wires integration
coverage into Codecov.
Current state:
- 7 integration tests in
internal/engine/covering HTTP happy path, HTTP 429 backoff, graceful shutdown, resource gate, DNS, PCAP, and WebSocket - CI
integrationjob runsgo test -race -tags integration -v ./internal/engine/... - Integration tests do not contribute to Codecov (no
-coverprofilein the job) - No cmd-level or CLI-level integration tests
Deliverables:
- Widen CI scope — change the integration job from
./internal/engine/...to./...so any future integration-tagged tests in other packages are automatically picked up - Codecov integration coverage — add
-coverprofile=integration-coverage.outto the integration CI job and upload to Codecov withflags: integration; this surfaces engine dispatch,Run, anddispatchcoverage that unit tests cannot reach - Hot-reload during dispatch — integration test that starts the engine, waits for
at least 3 requests, calls
Reload()with a new target list, then verifies subsequent requests hit the new target; exercises the live reload path under real concurrency - Burst mode +
--duration— integration test that configuresmode: burstwith a shortramp_up_sand runs the engine with a context timeout; verifies requests are dispatched and that the engine stops cleanly at the deadline - Output writer end-to-end — integration test that enables
output.enabledwith a temp JSONL file, dispatches ≥5 requests, and verifies the file contains valid newline-delimited JSON records with correcturl,status, andduration_msfields; complements the existing PCAP test - Per-domain rate-limit enforcement — integration test that sets a per-domain RPS of 2 against a local httptest server, dispatches requests over a measured window, and asserts the observed RPS does not materially exceed the configured limit; catches regressions in the rate-limit registry wiring
- cmd integration tests — test the Cobra commands directly (not via
exec.Command) using a shared test helper that invokesrootCmd.Execute()with args and a captured stdout buffer:validate— valid config → exit 0; invalid config → exit 1 with error textstart --dry-run— prints dry-run summary, does not start the enginegenerate --targets-file— emits valid YAML to stdout given a temp targets fileversion— prints version string
Not targeted:
probeandpinchnetwork integration (require live external endpoints)startfull run via CLI binary subprocess (covered by engine integration tests at the library level; binary-level testing deferred to a future E2E suite)
Surface per-test pass/fail data in Codecov so failed test names and messages appear directly in PR comments, removing the need to dig into CI logs.
Approach:
- Replace the raw
go testcall in the CItestjob withgotestsum, which wrapsgo testand emits a JUnit XML report alongside the existing coverage profile:gotestsum --junitfile junit.xml -- -race -coverprofile=coverage.txt -covermode=atomic ./...
- Add a second Codecov upload step using
codecov/test-results-actionwithif: ${{ !cancelled() }}so results are uploaded even when tests fail - Pin both the
gotestsuminstall and the action to commit SHAs (consistent with existing policy)
Features unlocked:
- Failed test names + failure messages shown in PR comments without opening CI logs
- Flaky test detection — tests that fail on
mainare flagged separately from new failures introduced by the PR - Per-test duration tracking over time in the Codecov dashboard
Terminal dashboard and commitment to a stable public API. By this point the OSSF Scorecard improvements (v0.12.x) will be in place; the Contributors check is expected to improve naturally as the project gains visibility following the TUI release.
- ✓ Live terminal UI using Bubble Tea behind a
--tuiflag; plain log output remains the default - ✓ Graceful fallback to plain logs when stdout is not a TTY (
ModeCharDevicedetection; zerolog silenced when TUI active) - ✓
internal/tuipackage:State(lock-free ring buffer + atomic counters), Bubble Tea model with sparkline,Runentry point - ✓
Engine.SetObserverhook — called after every dispatch; zero coupling to TUI internals - v1.0.0 marks a stability commitment: CLI flags, config schema, and Prometheus metric names will not have breaking changes without a major version bump
┌─ sendit ──────────────────────────────────────────────────┐
│ mode: human workers: 2/4 uptime: 00:04:32 │
├───────────────────────────────────────────────────────────┤
│ RECENT REQUESTS │
│ 200 GET https://httpbin.org/get 142ms 12 KB │
│ 200 DNS example.com 4ms │
│ 429 GET https://httpbin.org/status/429 201ms ↩ 8s │
│ 200 GET https://httpbin.org/get 98ms 9 KB │
├───────────────────────────────────────────────────────────┤
│ TOTALS requests: 312 errors: 4 bytes: 1.1 MB │
│ RATE LIMITS httpbin.org ████░░ 0.8 rps │
└───────────────────────────────────────────────────────────┘
Add a grpc driver so sendit can generate traffic against gRPC services alongside its existing HTTP, DNS, WebSocket, and browser drivers.
- New
type: grpctarget withurl: grpc://host:port/package.Service/Method - Unary RPC calls; request body supplied as a JSON string that is marshalled to protobuf via the gRPC reflection API (no
.protofiles required at runtime) - Response mapped to a synthetic status code:
0 → 200, gRPC status codes → HTTP-equivalent ranges so the existing error classifier and backoff logic work unchanged --tls,--insecure, and--authorityflags mirroring the HTTP driver's TLS options- Per-domain rate limiting and backoff apply to gRPC targets by hostname
- Pure Go —
google.golang.org/grpcwithgoogle.golang.org/grpc/reflection/grpc_reflection_v1;CGO_ENABLED=0preserved - Docs: new
grpcsection in the Drivers page; config examples
targets:
- url: grpc://localhost:50051/helloworld.Greeter/SayHello
type: grpc
weight: 10
grpc:
body: '{"name": "world"}'
tls: falsePer-target authentication so sendit can generate traffic against protected endpoints without manual header management.
authblock on any target:type: bearer | basic | header | querybearer: setsAuthorization: Bearer <token>; token supplied as a literal string or read from an env var (token_env: MY_TOKEN)basic: setsAuthorization: Basic <base64(user:pass)>; password optionally from env varheader: arbitrary header name + value (covers API keys,X-Api-Key, etc.)query: appends a key/value pair to the request URL query string- Auth config is redacted from
--dry-runoutput and logs - Applies to
httpandwebsocketdrivers; SFTP uses credentials from its ownsftpblock; other drivers ignore it
targets:
- url: https://api.example.com/data
type: http
weight: 5
auth:
type: bearer
token_env: API_TOKENVariable substitution in target URLs and request bodies so a single target definition can generate varied traffic without duplicating config entries.
varsblock on a target: a map of variable name → list of values; one value is chosen per request (uniform random or weighted)- Substitution syntax:
{{var}}inurl,http.body,grpc.body, andwebsocket.send - Built-in variables:
{{uuid}}(random UUIDv4),{{timestamp}}(Unix epoch seconds),{{seq}}(per-target incrementing counter) vars_file: load variable lists from a CSV or newline-delimited file- Dry-run output shows an example expanded URL for each templated target
targets:
- url: https://api.example.com/users/{{user_id}}
type: http
weight: 10
vars:
user_id: [alice, bob, carol, dave]A sendit replay subcommand that reads a JSONL result file produced by --output and re-issues the same requests as live traffic — useful for reproducing a traffic pattern, debugging a failure sequence, or warming a cache.
sendit replay --input results.jsonl— re-sends each request in the JSONL file in order--rateflag to replay at a fraction or multiple of the original rate (e.g.0.5for half speed,2.0for double)--filter status=5xxto replay only failed requests--loopto repeat the file in a continuous cycle- Uses the existing driver infrastructure — the appropriate driver is selected from the
typefield in each result record - Outputs a new JSONL file if
--outputis specified, enabling before/after comparison
# Replay last hour's failures at half speed
sendit replay --input results.jsonl --filter status=5xx --rate 0.5Explicit HTTP version selection for http targets. Today the driver uses Go's standard http.Transport, which automatically negotiates HTTP/2 over TLS via ALPN but provides no way to force or observe the negotiated protocol. HTTP/3 (QUIC) is not supported at all.
http_version: 1 | 2 | 3field under thehttp:target block (or as a top-leveltarget_defaults.http.http_version)1— force HTTP/1.1 (disableh2ALPN advertisement)2— force HTTP/2; fail fast if the server does not support it3— HTTP/3 over QUIC viagithub.com/quic-go/quic-go; plaintext and TLS both supported- Default (
0/ omitted) — current behaviour: HTTP/1.1 or HTTP/2 via ALPN negotiation; no HTTP/3 - Negotiated protocol logged at debug level and included in JSONL result output
- HTTP/3 is an optional build tag (
sendit_h3) to keep the default binary dependency-free; a separatesendit-h3binary is released alongside the standard binary
targets:
- url: https://example.com
type: http
weight: 5
http:
http_version: 2 # force HTTP/2; error if server does not support itLoad test SFTP file transfer infrastructure with multiple users, configurable file sizes, SSH handshake policy enforcement, and optional EICAR upload for malware scanner testing.
- Operations —
upload,download,list; set viasftp.operationper target - Auth —
sftp.username+sftp.passwordorsftp.private_key(file path or inline PEM string); multiple user targets with weights model realistic user-mix load - Payload sizing — for
upload:sftp.file_size_bytes(fixed) orsftp.file_size_min_bytes/sftp.file_size_max_bytes(random per request);BytesReadin results reflects actual bytes transferred for both upload and download - EICAR testing —
sftp.eicar: trueuploads the 68-byte EICAR standard test string instead of random data; result status reflects what the server returns, enabling detection of async vs synchronous AV scanner blocking - Connection caching —
ssh.Clientcached per address, username, auth material, and SSH policy with async.Mutex; stale connections are evicted and reconnected
Four fields added to JSONL output via task.Result.Meta (a new map[string]string field, merged inline into JSONL records):
| Field | Example | Source |
|---|---|---|
sftp_server_version |
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3 |
ssh.Conn.ServerVersion() |
sftp_host_key_type |
ssh-ed25519 |
HostKeyCallback |
sftp_host_key_fp |
SHA256:abc123... |
HostKeyCallback |
sftp_auth_methods |
publickey or password |
configured auth method |
For list, sftp_entry_count is also included in Meta.
Restrict which SSH algorithms the client will accept. If the server cannot satisfy the restriction, the handshake fails and the result is 502. Omit a field to accept all server-offered values.
sftp:
allowed_ciphers: [aes256-gcm@openssh.com, chacha20-poly1305@openssh.com]
allowed_kex: [curve25519-sha256]
allowed_host_key_types: [ssh-ed25519] # rejects RSA host keys → 502
allowed_macs: [hmac-sha2-256-etm@openssh.com]This enables scheduled policy probes — e.g., alert if a host key rotates from Ed25519 to RSA.
Host keys are verified against ~/.ssh/known_hosts by default. Set sftp.insecure: true only for trusted lab or ephemeral hosts where host-key pinning is intentionally bypassed.
| Condition | Code |
|---|---|
| Transfer success | 200 |
| Auth failure | 401 |
| Permission denied | 403 |
| File not found (download) | 404 |
| Host key rejected / policy mismatch | 502 |
| SFTP protocol error | 502 |
| Connection timeout | 504 |
target_defaults:
sftp:
port: 22
operation: upload
timeout_s: 30
insecure: false
targets:
- url: sftp://sftp.example.com/uploads/test.bin
type: sftp
weight: 10
sftp:
username: testuser
password: secret
file_size_min_bytes: 1024
file_size_max_bytes: 10485760
- url: sftp://sftp.example.com/uploads/eicar.txt
type: sftp
weight: 1
sftp:
username: testuser
password: secret
file_size_bytes: 68
eicar: true
- url: sftp://sftp.example.com/incoming
type: sftp
weight: 2
sftp:
username: auditor
private_key: /etc/sendit/audit_key
operation: list
allowed_host_key_types: [ssh-ed25519]
allowed_ciphers: [aes256-gcm@openssh.com, chacha20-poly1305@openssh.com]github.com/pkg/sftp— pure Go SFTP client, no CGOgolang.org/x/crypto/ssh— SSH transport and algorithm policy controls
Write the network traffic generated by a sendit session as a PCAP file for analysis in Wireshark or similar tools. Promoted from Research; see the Research section below for the full investigation notes. Closes #70.
- Synthetic PCAP from result data — generate a valid PCAP file from sendit's per-request telemetry (URL, timing, bytes, status); no root or
CAP_NET_RAWprivilege required; output is approximate (no TCP-level framing) but sufficient for replay and latency analysis in Wireshark --capture <file>flag onsendit start— write a PCAP to the specified path while the engine runs; file is finalised on clean shutdown or SIGTERMsendit export --pcap <results.jsonl>— post-run conversion of a JSONL result file to PCAP, enabling capture from any previous run- Output format — PCAP (
.pcap) for maximum tool compatibility - Docs — document the
--captureflag, the export subcommand, and the external-tooling alternative (tcpdump/tsharkalongside sendit; Kubeshark for Docker deployments)
Investigate adding a driver for non-standard or application-layer protocols that don't fit the existing HTTP/DNS/WebSocket/browser model.
Areas to explore:
- Protocol candidates: gRPC, raw TCP, ICMP, SMTP, FTP, custom binary protocols
- Whether a generic
rawdriver with a user-supplied payload and framing spec is preferable to per-protocol drivers - How RCODEs / response codes map to the existing unified error classifier
- Connection pooling and state management for connection-oriented protocols
- What a config schema for non-HTTP targets looks like (no URL scheme, port-based, payload templating)
Investigation complete; promoted to a versioned milestone. See v0.14.1 below.
Original research notes: investigate a burst or aggressive pacing mode for scenarios where politeness constraints should be relaxed — load testing, internal infrastructure, or controlled chaos experiments.
Areas to explore:
- A
burstmode that fires requests as fast as worker slots allow with no inter-request delay - Configurable concurrency ramp-up (e.g. linearly increase workers to max over a warm-up period)
- Whether the existing resource gate (
cpu_threshold_pct,memory_threshold_mb) is sufficient protection or needs a hard cap on total requests/duration - A
--durationflag forstartthat auto-stops after a fixed wall-clock time, useful for timed load runs - How backoff and per-domain rate limits interact with burst mode (bypass, warn, or error)
Investigation complete. Core feature shipped in v0.11.0; Safari bookmarks and fixture-based tests completed in v0.14.0. Related to #49 — the same browser automation knowledge applies to both driving traffic and sourcing targets.
Areas to explore:
- Chrome / Chromium history —
HistorySQLite file (urlstable,visit_countcolumn) located at:- Linux:
~/.config/google-chrome/Default/History - macOS:
~/Library/Application Support/Google/Chrome/Default/History - Chrome must be closed or the file opened read-only (SQLite WAL mode may allow concurrent reads)
- Linux:
- Chrome bookmarks —
BookmarksJSON file in the sameDefault/directory; parse therootstree recursively to extracturlentries - Firefox history —
places.sqlite(moz_placestable,visit_countcolumn) at~/.mozilla/firefox/<profile>/places.sqlite; bookmarks share the same file viamoz_bookmarks - Firefox profile discovery —
profiles.iniin the Firefox config dir; the default profile must be auto-detected when no explicit path is given - Safari — history in
~/Library/Safari/History.db(SQLite); bookmarks in~/Library/Safari/Bookmarks.plist(binary plist); macOS only - Cross-platform path resolution — abstract browser profile paths behind an OS+browser lookup so the same flag works on Linux and macOS without manual path configuration
- Filtering — HTTP/HTTPS URLs only; strip query strings and fragments optionally; de-duplicate by normalised URL; respect
--history-limit - Weight derivation — map
visit_countto a target weight (e.g. log-scaled) so high-traffic pages appear more frequently in generated traffic without dominating the distribution entirely - Privacy considerations — document that history/bookmark data never leaves the local machine; the generated
config.yamlcontains only URLs, not browsing metadata
Investigation notes for the packet capture feature shipped in v0.10.6. Related to #70.
Areas to explore:
- External tooling (short-term) — document how to run
tcpdumportsharkalongside sendit to capture traffic; for Docker deployments, Kubeshark or a tcpdump sidecar are natural fits; this requires no code changes - Synthetic PCAP from result data — sendit already collects URL, timing, bytes-read, and status code per request; investigate generating a valid PCAP file from this data without raw packet access; no root/CAP_NET_RAW privilege required; output would be approximate (no TCP-level detail) but sufficient for replay and latency analysis
- Live capture via gopacket — use
github.com/google/gopacket(libpcap bindings) to capture packets on the network interface filtered by the sendit process; requires root orCAP_NET_RAW; adds a heavy CGO dependency that conflicts with the currentCGO_ENABLED=0build - eBPF PID-filtered capture — use eBPF to capture only packets originating from the sendit PID; avoids libpcap but requires a modern Linux kernel (5.8+) and elevated privileges
- Output format — PCAP (
.pcap) for maximum tool compatibility; PCAPNG (.pcapng) if metadata per-packet is needed - Integration point — a
--capture <file>flag onsendit startor a post-runsendit export --pcapsubcommand
Shipped approach: synthetic PCAP from result data using LINKTYPE_USER0 (147) in pure Go (internal/pcap). No CGO, libpcap, or elevated privileges required. --capture <file> flag added to sendit start; sendit export --pcap <results.jsonl> added for post-run conversion.
The v0.10.6 synthetic PCAP provides request-level telemetry but no TCP/IP framing. Future work to investigate true packet-level capture:
- Live capture via gopacket — use
github.com/google/gopacket(libpcap bindings) to capture actual packets on the network interface, filtered by the sendit process; produces real PCAP with TCP/IP headers; requires root orCAP_NET_RAW; adds a CGO dependency that conflicts with the currentCGO_ENABLED=0build; consider a build tag to keep the default binary CGO-free - eBPF PID-filtered capture — use eBPF (e.g.
github.com/cilium/ebpf) to capture only packets originating from the sendit PID, avoiding the promiscuous-mode overhead of libpcap; requires a modern Linux kernel (5.8+ for BTF, 5.15+ recommended) andCAP_BPFor root; no CGO, but Linux-only - PCAPNG — upgrade the output format to PCAPNG (
.pcapng) if per-packet metadata (interface name, comment fields, custom blocks) is needed;PCAPNGis backwards-compatible with Wireshark but adds format complexity - Docker / Kubernetes — for containerised deployments, document how to use a
tcpdumpsidecar (or Kubeshark for Kubernetes) to capture traffic alongside sendit without modifying the binary
Review and enable GitHub's built-in security features to give the project a clear vulnerability disclosure process and broader automated dependency scanning.
Areas to explore:
- Security policy — add a
SECURITY.mddefining the supported versions and the process for reporting vulnerabilities (e.g. email or GitHub private reporting) - Private vulnerability disclosure — enable GitHub's private vulnerability reporting feature so reporters can submit CVEs without opening a public issue; evaluate whether the default advisory workflow fits the project
- Dependabot alerts — confirm Dependabot security alerts are enabled (distinct from the Dependabot version-update PRs already in place); review alert thresholds and whether auto-dismiss rules are appropriate
- Branch protection hardening — review current branch protection rules on
mainfor gaps (e.g. required signed commits, dismiss stale reviews on push) - OSSF Scorecard — evaluate adding the OpenSSF Scorecard action to surface a public supply-chain security score
- Docs site — security page — add a dedicated Security page to the docs site summarising the security policy, supported versions, and how to report a vulnerability; link from the homepage and CLI reference
- Docs site —
security.txt— add a/.well-known/security.txt(RFC 9116) to the GitHub Pages site (docs/static/.well-known/security.txt) so automated scanners and researchers can discover the disclosure contact and policy URL machine-readably
Investigate extending the browser driver to support Firefox and WebKit/Safari in addition to the current Chrome/Chromium. Deferred from v0.14.3 after research (March 2026) concluded no viable path exists today that is compatible with sendit's statically compiled, CGO-free, single-binary distribution model.
Full research findings are in #49. Summary of why each option was rejected:
- playwright-go (
github.com/playwright-community/playwright-go) — spawns a bundled Node.js subprocess at runtime; cannot be embedded in a static Go binary; incompatible withCGO_ENABLED=0distribution - Firefox via chromedp (CDP) — Firefox dropped CDP support in Firefox 129 (mid-2024); removed from the Selenium ecosystem in early 2025; chromedp has no WebDriver BiDi implementation
- rod (
github.com/go-rod/rod) — Chromium-only; same limitation as current chromedp; no multi-browser gain
Unblocking condition: A production-ready, CGO-free Go client for WebDriver BiDi (the cross-browser successor to CDP). Chrome, Firefox, and Safari all support or are implementing BiDi. Once a viable Go BiDi library emerges, revisit this item with an engine: chromium|firefox|webkit field under the browser: target block.
Areas to re-evaluate when revisiting:
- Go WebDriver BiDi client maturity (watch
seleniumhq/seleniumGo bindings and community alternatives) - Per-task allocator model compatibility — does the library support spawn-per-task or require a shared browser instance?
- Headless browser availability on
ubuntu-latestfor non-Chromium engines - Docker image strategy — single image vs separate
browser-expandedtag with pre-installed browsers