Skip to content

Releases: garagon/aguara

v0.14.5

24 Apr 22:25
5377f6b

Choose a tag to compare

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. Their internal/engine/scanner.go already 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.Statos.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 with WithRedaction(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).ScanContentAs scrub credential-leak matches before returning. Pass WithRedaction(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 *.ext fast path. Custom rules with skills/**/*.md or .github/workflows/*.yml silently do not fire. Depends on the match_mode proximity work already planned as T1-01.
  • Decoder-cap bypass via benign-padding. An attacker can push a malicious encoded blob past the maxBlobsPerFile=10 cap 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

24 Apr 13:50
885cf5f

Choose a tag to compare

What's Changed

  • fix(docker): run as non-root and pin base image digests by @garagon in #60
  • fix(pattern): use overlapping AC iteration in keyword prefilter by @garagon in #61
  • release: v0.14.4 by @garagon in #62

Full Changelog: v0.14.3...v0.14.4

v0.14.3

21 Apr 13:50
ac0560f

Choose a tag to compare

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) matched DAN inside 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-refgithub.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.sh

What'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

19 Apr 01:34
2d34f38

Choose a tag to compare

What's Changed

  • fix(docker): strip v prefix from injected version (v0.14.2) by @garagon in #54

Full Changelog: v0.14.1...v0.14.2

v0.14.1

19 Apr 00:19
54d22bd

Choose a tag to compare

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

18 Apr 22:49
4188d83

Choose a tag to compare

What's Changed

  • docs(readme): use docker buildx imagetools to read image SBOM + provenance by @garagon in #52

Full Changelog: v0.14.0-rc2...v0.14.0

v0.13.0

26 Mar 15:38
d06559e

Choose a tag to compare

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

25 Mar 03:03
2be2824

Choose a tag to compare

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

25 Mar 00:56
a7c8f35

Choose a tag to compare

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 check always 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 clean defaults 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

24 Mar 20:54
1a95114

Choose a tag to compare

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 output

Detects:

  • 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 caches

Actions:

  • 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