Releases: garagon/aguara
v0.14.5
What's new
Four audit items surfaced by an external review of v0.14.4, plus incidental dev-tooling cleanup. One behavior change library consumers must know about (credential-leak matched_text is now scrubbed by default), one new CLI flag (--no-redact), one new library option (WithRedaction).
Credential-leak findings are redacted by default
Detecting a secret and then writing it verbatim to terminal output, JSON, SARIF, or an -o file creates a second copy of the secret in a location that often has weaker access controls than the original: CI logs retained for days, GitHub Code Scanning history, Slack notifications, shared results.json files checked into git by accident. The scan artifact becomes the leak.
Finding.MatchedText and any Context lines marked is_match=true are now replaced with the literal string [REDACTED] when the finding's category is credential-leak. Rules in other categories are untouched because their match is typically a pattern signature (ignore previous instructions) rather than a secret that needs protecting.
Behavior change for library consumers. Code parsing matched_text of a CRED_* finding as the credential value itself will now see [REDACTED]. Known consumers:
oktsec: no code change required. Theirinternal/engine/scanner.goalready redacts credentials; double-redaction stays[REDACTED].aguara-mcp: no code change required. Having credentials scrubbed before crossing the MCP boundary is strictly better for AI-agent consumers.
Consumers that genuinely need the raw match pass aguara.WithRedaction(false) (library) or --no-redact (CLI).
Update check auto-suppresses in recognized CI environments
The GitHub Action already passed --no-update-check, but anyone running the bare binary inside CI (Dockerfile, Makefile, ad-hoc script) was hitting the GitHub Releases API on every run. Leaked timing and user-agent metadata from supposedly-isolated environments, and fights the "no network, no LLM" positioning.
Execute() now flips flagNoUpdateCheck automatically when CI=true (the de-facto standard: set by GitHub Actions, GitLab, CircleCI, Travis, Buildkite, Bitbucket Pipelines, Drone, Woodpecker, and most others), or when any of GITHUB_ACTIONS, GITLAB_CI, CIRCLECI, BUILDKITE, JENKINS_URL, TEAMCITY_VERSION, TRAVIS is set. CI=false/CI=0/empty CI= are correctly ignored. Local invocations still get the notice; --no-update-check and AGUARA_NO_UPDATE_CHECK=1 remain as explicit opt-outs.
--changed scan no longer follows committed symlinks
The regular directory walk in internal/scanner/target.go rejects symlinks via info.Mode()&os.ModeSymlink. scanChangedFiles got its paths from git and used os.Stat, which resolves symlinks to their target. A symlink committed to the repo pointing at /etc/passwd or ~/.ssh/id_rsa would be followed on the next --changed CI run and the target's contents would surface in findings (and any SARIF upload to GitHub Code Scanning).
Fix: os.Stat → os.Lstat, skip entries where the mode bit is set. Regression test creates a git repo with a symlink pointing to an out-of-tree secret and asserts the symlink is not scanned.
.gitignore hygiene + contributor self-scan config
Prophylactic: .gitignore now covers .env, .env.*, *.pem, *.key (with .env.example allow-listed so templates stay trackable). git log --all confirms the repo has never contained such files, but a scanner's own repo really should not ship a misplaced credential file by accident.
Aguara is a scanner whose own source intentionally contains attack-pattern signatures (rule YAML examples.true_positive blocks, testdata/, sandbox/, documentation). A clean aguara scan . against the repo produced ~9,600 findings dominated by by-design content. A repo-root .aguara.yml now scopes contributor self-scans to production code paths (~63 findings, all in test files that embed payloads). CONTRIBUTING.md gained a Running Aguara on this repo section.
Library API
New:
aguara.WithRedaction(enabled bool) Option- opt-out withWithRedaction(false).types.RedactedPlaceholder- string constant, value[REDACTED].types.RedactCredentialFindings([]Finding)- exposed for consumers that want to apply the same redaction to findings obtained via other code paths.
Changed:
aguara.Scan,aguara.ScanContent,aguara.ScanContentAs,(*Scanner).Scan,(*Scanner).ScanContent,(*Scanner).ScanContentAsscrub credential-leak matches before returning. PassWithRedaction(false)to preserve the previous behavior.
No signature changes. No removed symbols. No rule-count change (still 189). No engine changes.
Not in this release
Two P2 audit items are deferred to v0.15.0 Tier 1:
- Rule target globs beyond the
*.extfast path. Custom rules withskills/**/*.mdor.github/workflows/*.ymlsilently do not fire. Depends on thematch_modeproximity work already planned as T1-01. - Decoder-cap bypass via benign-padding. An attacker can push a malicious encoded blob past the
maxBlobsPerFile=10cap by prepending benign blobs. Needs perf benchmarks before raising the cap or adding hash-based dedup.
Full Changelog: v0.14.4...v0.14.5
v0.14.4
v0.14.3
Maintenance release. Bundles one install-reliability fix, four rule calibration tweaks, a noisy update-check message, and a hardening change to the composite action. No engine changes, no rule-count change. There is no CVE, no known exploitation, and no action required beyond upgrading normally.
Fixed
Fresh installs of v0.14.0 / v0.14.1 / v0.14.2 were failing
install.sh extracted the expected checksum with grep "$file" checksums.txt | awk '{print $1}'. After v0.14.0 started shipping per-archive SBOMs, the substring grep also matched the sibling .sbom.json line, so awk '{print $1}' returned two hashes concatenated. Every install aborted with checksum mismatch: expected <hash1><hash2>, got <hash1>. The script was failing closed - no one was silently compromised - but nobody could install Aguara fresh.
Fix: exact-filename match on column 2 with awk. Users who already had v0.14.x installed (via Homebrew, go install, or a pre-v0.14 install.sh) were unaffected.
Four rule false positives on real-world skill docs
Detection-engineering pass over a 1247-file corpus of real skills caught four regexes firing on legitimate content without any corresponding true-positive loss:
PROMPT_INJECTION_004(Zero-width char obfuscation) fired on a single UTF-8 BOM at file start. Pattern 2 now requires{2,}like pattern 1.PROMPT_INJECTION_011(Jailbreak template) matchedDANinside unrelated words -Enable zone reDANdancy,clippy::peDANtic. Tokens are now anchored with\b.UNI_001(RTL override) fired on U+202D (LRO), which appears in legitimate mixed-direction layout. Narrowed to U+202E (RLO, the actual Trojan Source signal).UNI_006(Tag characters) had a range that missed U+E0000 (LANGUAGE TAG). Extended to the full Unicode Tag Characters block.
All true-positive coverage preserved. testdata/malicious/ still produces 98 findings, unchanged.
Update available: v0.14.2 → v0.14.2 on every invocation
The ldflag-injected binary version came in as 0.14.2 while the GitHub Releases API returns v0.14.2. The equality check compared them as raw strings, so up-to-date binaries kept printing an "update available" line pointing to the same version they were running. Fix: strip the leading v on both sides before comparing.
The tag_name returned by the GitHub API is now also validated against ^v\d+\.\d+\.\d+$ before being displayed, so a future hijacked release page cannot surface arbitrary text in the user's terminal.
Changed
action.yml no longer pulls install.sh from main
The composite action previously fetched install.sh directly from the main branch on every consumer run. That is a poor supply-chain pattern - a future compromise of the repository's write access would propagate to downstream CI without a release ever being cut, bypassing the Cosign/SBOM/SLSA signing pipeline that covers the tagged path. This is a hardening change, not a response to any observed incident.
The action now resolves the install ref from inputs.install-script-ref → github.action_ref → a baked-in tag default, rejecting anything that is not a semver tag (vX.Y.Z) or a 40-char commit SHA. @main, @v1, @<branch> all fall back to the pinned default and emit a GHA ::warning::. Consumers who pin uses: garagon/aguara@v0.14.3 (or any exact tag or SHA) see no behavior change.
DEFAULT_REF is bumped to v0.14.3 so consumers using non-semver refs fall back to this release's fixed install.sh.
Upgrade
- Homebrew:
brew update && brew upgrade aguara - go install:
go install github.com/garagon/aguara/cmd/aguara@v0.14.3 - install.sh (fresh):
curl -fsSL https://raw.githubusercontent.com/garagon/aguara/v0.14.3/install.sh | bash - Docker:
docker pull ghcr.io/garagon/aguara:0.14.3 - GitHub Action:
uses: garagon/aguara@v0.14.3
Verification
Post-release acceptance script passed all 6 checks on darwin/arm64: Cosign-signed checksums, archive sha256, extracted binary version, Cosign-signed Docker image, native multi-arch pull, SBOM + SLSA provenance attestations.
Reproduce locally:
VERSION=v0.14.3 .github/scripts/verify-release.shWhat's Changed
- fix(action): pin install.sh fetch to a tagged ref, never main in #56
- fix(install): exact-filename match in verify_checksum in #56
- fix(rules): tighten regex boundaries on four unicode + jailbreak rules in #57
- fix(update-check): normalize v-prefix + validate tag shape in #58
- release: v0.14.3 in #59
Full Changelog: v0.14.2...v0.14.3
v0.14.2
What's Changed
Full Changelog: v0.14.1...v0.14.2
v0.14.1
What's Changed
- fix(docker): inject version + multi-arch build + release acceptance script (v0.14.1) by @garagon in #53
Full Changelog: v0.14.0...v0.14.1
v0.14.0
What's Changed
Full Changelog: v0.14.0-rc2...v0.14.0
v0.13.0
Cached Scanner API - 82% faster library-mode scanning
New NewScanner() API that compiles rules, regex patterns and search structures once at startup. Scanner.ScanContent() reuses the cached matcher on every request. Thread-safe, backwards compatible.
Benchmarks (benchstat, 6 iterations, p=0.002)
| Scenario | v0.12 | v0.13 | Change |
|---|---|---|---|
| Short message | 9.7ms | 0.7ms | -92.5% |
| JSON config | 11.2ms | 1.9ms | -82.6% |
| Structured markdown | 13.4ms | 4.3ms | -68.0% |
| Plain text | 11.3ms | 2.3ms | -79.8% |
| Latency geomean | 9.7ms | 1.7ms | -82.7% |
| Concurrent (8 threads) | 1.9ms/op | 0.08ms/op | -95.6% |
| Memory per scan | 13.2MB | 9KB | -99.9% |
Usage
// Build once at startup
scanner, err := aguara.NewScanner(opts...)
// Per-request (reuses compiled rules, thread-safe)
result, err := scanner.ScanContent(ctx, content, "message.md")Other improvements
- Decoder rescan filtered to all-target rules only (skip extension-specific rules for decoded content)
- NLP fast-path: skip Goldmark parsing for structureless plain text while preserving authority claim and credential exfil combo detection
- Post-processing: early return on 0 findings, O(n log n) proximity check replacing O(n^2)
Full API
scanner.ScanContent(ctx, content, filename)
scanner.ScanContentAs(ctx, content, filename, toolName)
scanner.Scan(ctx, path)
scanner.ListRules()
scanner.ExplainRule(id)
scanner.RulesLoaded()Existing aguara.ScanContent() and aguara.Scan() package-level functions still work unchanged.
Full Changelog: v0.12.1...v0.13.0
v0.12.1
v0.12.1 - Fix .pth false positives
Fixes false positives where aguara check flagged legitimate Python ecosystem .pth files as CRITICAL.
Problem
_virtualenv.pth (present in every virtualenv and uv cache) contains import _virtualenv, which triggered the executable .pth detection. Users running aguara check got dozens of CRITICAL findings from their uv cache, all false positives.
Fix
Allowlist of known-safe .pth files that legitimately use import statements for site customization:
_virtualenv.pth(virtualenv)distutils-precedence.pth(setuptools)easy-install.pth(setuptools legacy)setuptools.pth(setuptools)coverage.pth(coverage.py)zope-nspkg.pth(zope.interface)
Malicious .pth files (subprocess, exec, eval, etc.) are still detected as CRITICAL.
Full Changelog: v0.12.0...v0.12.1
v0.12.0
v0.12.0 - uvx Detection + Incident Response UX
Covers the exact litellm attack vector (uvx auto-download of compromised packages) and simplifies incident response to two commands.
New rules
| Rule | Severity | Description |
|---|---|---|
| MCPCFG_012 | HIGH | uvx/uv MCP server without version pin. Detects configs where uvx auto-downloads the latest version from PyPI on every run. |
| MCPCFG_013 | MEDIUM | pip install without --require-hashes in MCP server setup. |
155 real MCP servers in Aguara Watch use uvx/uv without version pins. All vulnerable to the same vector that compromised litellm.
Incident response improvements
aguara checkalways scans caches - uv, pip, and npx caches are now scanned by default (no flag needed). Cache scanning reads METADATA in dist-info directories and checks .pth files for executable content.aguara cleandefaults to Y - confirmation changed from[y/N]to[Y/n]. Press Enter to proceed.
The workflow is now two commands, zero flags:
aguara check
aguara clean
Stats
189 rules, 576 tests, 0 lint issues. Validated against 28,000+ skills with 0 false positives.
Full Changelog: v0.11.1...v0.12.0
v0.11.1
v0.11.1 - Incident Response Commands
Two new commands for detecting and cleaning compromised Python packages. Built in response to the litellm supply chain attack.
aguara check
Scans installed Python environments for compromised packages and persistence artifacts.
aguara check # auto-discover Python environment
aguara check --path /opt/venv/... # specific site-packages directory
aguara check --include-caches # also check pip/uv caches
aguara check --format json # machine-readable outputDetects:
- Known compromised package versions (currently litellm 1.82.7/1.82.8)
- .pth files with executable content (import, subprocess, exec, eval)
- Persistence backdoors (~/.config/sysmon/, systemd user services)
- Reports which credential files exist on the system with rotation guidance
aguara clean
Removes compromised packages and quarantines malicious files.
aguara clean --dry-run # preview without changes
aguara clean # interactive confirmation
aguara clean --yes --purge-caches # non-interactive, purge pip/uv cachesActions:
- Uninstalls compromised packages via pip or uv
- Quarantines malicious .pth files to
/tmp/aguara-quarantine/(preserves forensic evidence) - Disables and quarantines systemd persistence services
- Prints credential rotation checklist (SSH, AWS, K8s, Git, npm, PyPI)
- Never deletes credential files or rotates credentials automatically
Stats
- 187 rules, 574 tests, 0 lint issues
Full Changelog: v0.11.0...v0.11.1