Skip to content

What's new

A version-by-version history of Fendix. v3.7.0 shipped October 2026: the GitHub Action fails the job when Semgrep cannot run, and a fatal Semgrep run is recorded as failed instead of passing as a clean scan. Each entry below lists what changed in that release.

v0.12.0 Complete

Unified Go/JS/IaC textscan SAST engine

May 18, 2026

  • Unified regex SAST engine. A single codebase drives Go, JavaScript/TypeScript, Dockerfile, and Kubernetes YAML rules using shared scanner scaffolding. 16 rules total across 4 language targets: Go rules (SQL injection via string concat, exec.Command shell invocation, weak hash for passwords, hardcoded AWS key ID); JS/TS rules (eval with non-literal arg, innerHTML from non-literal, child_process.exec, document.write, require with non-literal path, hardcoded AWS key); IaC rules (Dockerfile FROM without privilege drop, ADD vs COPY, :latest tag; Kubernetes privileged: true, hostNetwork: true, allowPrivilegeEscalation: true, runAsUser: 0).
  • Extension-based routing. Filename extension determines which rule set applies: .go → Go rules; .js / .ts / .jsx / .tsx → JS rules; Dockerfile / *.dockerfile → Docker rules; .yaml / .yml → Kubernetes rules (heuristic: skips files that look like GitLab/CircleCI CI configs). Skips noisy build directories: node_modules, vendor, .git, build, dist.
  • Pure stdlib — no new deps, no CGo. The textscan engine is wired into the orchestrator between the Semgrep and Python passes; runs whenever --code is set. Go XXE and insecure-rand rules, JS prototype-pollution and insecure-RNG rules, and Terraform HCL support are deferred follow-ups requiring either AST context or MPL-2.0 dep acceptance.
v0.11.1 Complete

Trust fixes — pip-audit naming, OSV batch queries, verify scope

May 14, 2026

  • pip-audit naming gap fixed. The Python dep-CVE scanner was marking every finding from the in-process pip-audit path with the tool's internal name instead of the human-facing advisory title. Findings now surface as <package>@<version>: <CVE-ID> — <advisory title> matching what users see in pip audit output. New --no-pip-audit-fallback flag disables the local-list fallback for strict environments.
  • OSV batch queries for pip and npm. The PyPI and npm dep-CVE scanners now send a single POST /v1/querybatch request with all (package, version) pairs instead of N sequential single-package queries. Typical reduction: 30 packages × 1 query → 1 batch query. Both scanners respect a new --osv-concurrency <n> flag (default 4) to cap parallel batch requests when the project has many lockfiles.
  • `fendix verify` scope fixed. fendix verify --id SEC-042 now exits 1 when the finding is confirmed (vulnerability still present) and exits 0 when it is resolved — matching the documented intent. Previously the exit codes were inverted. New explicit --confirmed-ok flag for the rare case where an operator wants the CI job to pass even on confirmed findings.
Engine evaluation Complete

Engine evaluation — 3-track real-world accuracy scorecard

May 13, 2026

  • Three independent evaluation tracks against v0.11.0 — synthetic precision/recall against canonical patterns, real-world DAST against OWASP Juice Shop, real-world SAST against PyGoat's Django OWASP Top 10 demo. Together they record what that version detected on each target; the caveats below and on the /accuracy page bound what the numbers mean. Headline numbers and the methodology now live on a dedicated <Link href="/accuracy">/accuracy</Link> page.
  • Track 1 — Synthetic labeled corpus (56 cases, 7 categories): F1 = 1.000, precision = 1.000, recall = 1.000. 38 true-positives / 0 false-positives / 0 false-negatives across sqli / cmdi / path-traversal / ssrf / open-redirect / xss / secrets. The corpus exists in scripts/accuracy/corpus/; ground truth in scripts/accuracy/manifest.json; harness in scripts/accuracy/run.py.
  • Track 2 — OWASP Juice Shop refresh vs v0.6.1 baseline: 12 findings (5 CRITICAL / 4 MEDIUM / 2 LOW / 1 INFO) in 27 s. +5 CRITICALs vs v0.6.1, scan duration −35 %. All 5 new CRITICALs are exposed-config-file detections (CWE-538): .DS_Store, .env family (3 paths), .git/* (3 paths), .htaccess, .htpasswd. Caveat: juice-shop's SPA returns 200 for unknown paths, so these may be SPA-fallback responses and are not counted as confirmed exposures.
  • Track 3 — PyGoat real-world SAST: 147 findings in 17.1 s on a 52-Python-file Django app. 1 CRITICAL (unsafe pickle deserialization), 146 HIGH. Categories: 135 deps (real CVE-tagged: certifi / cryptography / django), 9 injection (eval / subprocess(shell) / SSRF / innerHTML XSS / yaml.unsafe_load / open-redirect at 9 sites / pickle), 3 secrets. Findings were reported in every OWASP Top 10 category PyGoat advertises; PyGoat has no machine-readable ground truth, so this is category coverage, not precision or recall.
  • 5 real engine improvements shipped during the evaluation arc: (1) _is_open_redirect upgraded to taint-chain posture parity — pre-fix the detector only matched direct redirect(request.args.get('x')); multi-hop assignments were silently missed; the other 6 reachable sinks already had the chain treatment. Open-redirect recall: 0/3 → 3/3. (2) cmd-injection posture aligned with the other reachable sinks via new _cmdi_arg_is_dangerous helper — os.system('echo hello') no longer fires HIGH on literal-string args. Precision: 0.833 → 1.000. (3) Orchestrator runWhiteboxScan now resolves code_path and spec to absolute paths before sending the ScanRequest — pre-fix the Python subprocess silently saw 0 files on every real codebase using --python-engine without FENDIX_ENGINE set. (4) run-juice-shop.sh portability fix: replaced GNU timeout (not on macOS) with --max-duration. (5) 3 cmdi unit tests updated to assert the new no-emit-on-literal posture.
  • Caveats documented honestly. The synthetic 1.000 means fendix never misses these 56 canonical patterns — not that it never misses anything. PyGoat lacks a machine-readable ground-truth manifest so we report category coverage rather than precision/recall. Juice Shop's SPA-fallback means the CRITICALs could be SPA-fallback responses, so they are not counted as confirmed exposures. The full caveat list + follow-up backlog lives in docs/accuracy.md.
v0.11.0 Complete

FP discipline + path-traversal reachability

May 13, 2026

  • The engine kept its fast cold-start while adding three new detection paths. This release ships false-positive round 2 plus path-traversal detection, and the engine held its v0.8.0 cold-start (6.1 ms p50 default) while adding three new detection paths on top.
  • Native-Go exposed-config-file scanner. New internal/scanner/configleak.go (~280 LOC). 32 patterns (27 basename + 5 directory prefix) cover .env / .env.local / .env.production, web-server config (.htaccess, .htpasswd, web.config), package-manager creds (.npmrc, .pypirc, .netrc), Docker (docker-compose.override.yml), IDE/OS leftovers (.DS_Store), and directory-style leaks (.git/, .aws/, .ssh/). Fires CRITICAL with CWE-538 on any 2xx response to a known config-file path. Body sample (capped 512 bytes) gets [REDACTED] masking of common secret-shape tokens before landing in evidence — no engine leak of leaked credentials. Inverts the prior FP shape where the engine would emit noisy MEDIUM missing-CSP-on-/.env findings into one CRITICAL exposed config file finding. 9 race-clean unit tests.
  • Path-traversal as the 7th reachable taint-chain sink class. New SEC-PY_PATH_TRAVERSAL finding (CWE-22, HIGH severity, MEDIUM confidence, category=injection). Four filesystem-path sinks recognised: open(x) (stdlib), Path(x) / pathlib.Path(x), send_file(x) (Flask), send_from_directory(safe_dir, x) (Flask — the user-controlled arg is at index 1, handled by _path_traversal_arg_index). When user input flows to the path arg, _collect_taint_chain records the chain and sets reachable: true; both the correlator and orchestrator severity escalations apply. Brings the engine's reachable sink categories to 7: SQLi / SSRF / open-redirect + XSS + cmd-injection + path-traversal. 9 new Python tests.
  • `fendix ignore` subcommand tree (internal/ignorecmd/, ~300 LOC). Three subcommands close the suppression-bookkeeping gap: fendix ignore list renders a tabular view with EXPIRED / expiring-soon (within 30 days) / active / no-expiry / INVALID-DATE status; fendix ignore validate reports schema and date errors with non-zero exit for CI gating; fendix ignore prune [--dry-run] removes expired rules and rewrites the file (preserves rules with invalid dates — validate surfaces those). All three default to .fendix-ignore in cwd; --file <path> targets a different file. 14 race-clean unit tests.
  • Cold-start benchmark refreshed. Default v0.11 = 6.1 ms p50 (+0.5 ms vs v0.9, within noise — configleak doesn't fire with zero discovered endpoints on the secrets-only fixture). --python-engine opt-in v0.11 = 40.7 ms p50 (+16.3 ms vs v0.9 — real cost of the new path-traversal sink in the AST analyzer). Net: default v0.11 is still 16 % faster than v0.8.0 while adding three new detection paths on top.
  • Spawner bug fix. internal/engine/spawner.go could compose python/python/engine.py for users on the local-fallback path because both engineDir and the relative script path were used together as cmd.Dir + arg. Fixed by resolving via filepath.Abs before composing. Closes a silent exit-2 for every --python-engine invocation without FENDIX_ENGINE set.
  • FP corpus re-triaged. No new shipping artifact, but the false-positive corpus catalog now records which of the 35 catalogued FPs are addressed, which the new config-file scanner picks up (dotfile inversion), and which is deferred until real post-launch user data exists (SPA-fallback dedup).
v0.10.0 Complete

Plugin ecosystem polish

May 13, 2026

  • The plugin ecosystem is genuinely usable now. The plugin wire contract from v0.7 is unchanged; what changed is the surface around it. An external author can ship a plugin against an installed binary in 60 seconds — copy-paste quickstart in the rewritten author docs, two new reference plugins in Node + Ruby prove the contract is language-agnostic, fendix plugins subcommand tree handles install + list, CI smoke test catches wire-contract regressions automatically.
  • `docs/plugins.md` rewritten for external authors. ~580 LOC restructured around the outside-contributor audience. 60-second copy-paste quickstart (TODO-comment finder, ~30 LOC) before any conceptual content; new 'How plugins fit into a scan' pipeline diagram; Python / Node / Bash skeleton entrypoints; 'Testing your plugin locally' section with inside-engine and outside-engine smoke-test recipes; 'Common errors' 8-row diagnosis table; 'Distributing your plugin' covering both git clone and fendix plugins install; tightened security model with explicit auth-token handling guidance; authoring checklist expanded 8 → 13 items.
  • Two new reference plugins in non-Go languages. examples/plugins/license-header-check/ (Node, stdlib-only, ~150 LOC) walks the source tree and flags files lacking an SPDX-License-Identifier header. examples/plugins/dockerfile-best-practices/ (Ruby, stdlib-only, ~210 LOC) walks for Dockerfile / Dockerfile.* and emits up to 5 distinct findings per file (:latest tag, curl | sh, ADD <url>, root-by-default, missing HEALTHCHECK). Both ship with a README. Reference-plugin shelf is now 5 plugins covering Python ×2, Bash ×1, Node ×1, Ruby ×1.
  • `fendix plugins` CLI subcommand tree. fendix plugins list enumerates discovered plugins (NAME / VERSION / MODE / DIR) using the same discovery roots a real scan uses — what's printed is exactly what would run. fendix plugins install <git-url> is a thin wrapper over git clone --depth=1: derives the on-disk name from the URL (handles .git suffix, scp-style git@host:org/repo, query strings, trailing slashes), refuses to overwrite preexisting directories, validates the cloned tree's plugin.yaml after clone, and removes the directory on validation failure so users never end up with a half-installed plugin that WARNs every scan. Also includes a symlink-discovery fix: plugin discovery now os.Stats each entry before IsDir()-checking, so symlinked plugin dirs work too.
  • Plugin smoke test in CI. New internal/e2e/refplugins_test.go (build-tag e2e, ~330 LOC) under the existing make e2e umbrella. One subtest per reference plugin (5 total) that copies the plugin into a temp scan root, runs fendix scan against a deterministic fixture, and asserts the expected findings flow through with the engine-attached fendix-plugin:<name> provenance tag. Each subtest skips cleanly when its required runtime (node, ruby, python3, bash + jq) isn't installed on the CI runner — so the test still passes on minimal images while exercising every plugin on full ones. Catches wire-contract regressions before plugin authors hit them.
v0.9.0 Complete

Cold start under 6 ms, no Python required

May 13, 2026

  • Secrets and semgrep checks now run as native Go in-process scanners; the embedded Python distribution is no longer bundled in the binary. Default cold start = 5.6 ms p50 (was 7.3 ms on v0.8 — 23 % faster). fendix no longer carries a Python interpreter requirement at all in the default scan path. New --python-engine opt-in flag re-enables the Python auth / injection / deps checks for users who still want them — costs ~24.4 ms p50 (4.4× the default; that gap is the actual cost of Python interpreter startup + engine extraction we removed). All numbers reproduced via scripts/bench/coldstart.py.
  • Native Go secrets scanner. New internal/scanner/secrets/ package ports the Python secrets analyzer to in-process Go: all 15 patterns (7 generic + 8 provider-specific — GitHub ghp_* / ghs_*, Stripe sk_live_*, Slack xox*, Google AIza*, Anthropic sk-ant-*, OpenAI sk-* / sk-proj-* / sk-svcacct-*, npm npm_*, GCP service-account JSON) plus the .env-only ENV_SECRET regex, walker with skip-dirs / extension gate / .env-name match / 1 MB cap / minified-JS-line skip, and evidence truncation. Same SEC-<PATTERN_ID> finding IDs as the Python implementation so any overlap (user explicitly passes --checks secrets) dedupes cleanly. Go RE2 doesn't support (?<![A-Za-z0-9]) lookbehinds, so the 9 provider-prefix patterns carry a boundaryOK post-match validator instead. Real-world parity verified against the Python fixture suite: 30 unique (title, endpoint) tuples emitted by both engines, set-diff empty in both directions. 24 race-clean unit tests.
  • Semgrep shelled-out, not embedded. New internal/scanner/semgrep/ package wraps the host's installed semgrep binary instead of running it through the embedded Python engine. The fendix rule pack (auth.yaml / injection.yaml / secrets.yaml) is bundled into the Go binary via //go:embed and extracted to a per-process temp dir on first scan. Result mapping mirrors the Python wrapper byte-for-byte: SEC-<RULE_ID> IDs, metadata.fendix_severity preferred over Semgrep's ERROR / WARNING / INFO mapping, evidence truncated at 200 chars, title at 120. Graceful absence: exec.LookPath("semgrep") failure returns ErrSemgrepUnavailable and the orchestrator logs an install hint and continues. Non-fatal exit codes (1 for matches, 2 / 5 / 7 for rule-parse errors that still emit valid JSON) are absorbed in parity with the Python wrapper. 28 race-clean unit tests cover mapping, graceful absence, ctx cancellation, fake-semgrep happy path, and rule embedding extraction.
  • Embedded Python distribution dropped from binary + redundant Python wrappers deleted. The Makefile's embed-engine target no longer copies python/ into the embedded engine tree; the binary's //go:embed directive bundles only a placeholder. The now-redundant Python secrets and semgrep-runner wrappers and their test files were deleted (215 → 159 Python tests; the 56 deleted tests were strictly the redundant wrapper coverage). Python whitebox spawning is now opt-in via the new --python-engine CLI flag, requires a local python/ source tree (or explicit FENDIX_ENGINE env var), and is silently skipped when no Python engine is resolvable. New scripts/bench/coldstart.py reproduction harness; docs/benchmarks.md adds a cold-start latency section with the table, methodology, binary-size delta (-99 KB), and re-run instructions.
  • Plugin wire-contract compatibility audit. All three reference plugins (custom-secret-pattern, custom-blackbox-check, custom-semgrep-pack) re-verified end-to-end against the new binary: discovery works, NDJSON in/out works, findings flow through correlation + dedup unchanged. Plugins do not depend on the embedded engine, the extracted ~/.fendix/engine/ tree, or --python-engine being set. Pre-existing limitation surfaced and documented: os.ReadDir().IsDir() returns false for symlinked plugin directories, so plugins must be installed as real directories (cp -R / git clone, not ln -s). Documented in docs/plugins.md.
  • No backend / frontend schema delta. The Go secrets and Go semgrep scanners emit findings with the same SEC-* IDs and the same Finding fields as the prior Python path. Dropping the embedded Python distribution doesn't touch the wire shape — only the runtime topology and the cold-start cost. Plugin findings still flow through the same Finding shape. So this release ships frontend-side as version-literal bumps + this changelog entry, and backend-side as a no-op (the existing _finding_defaults ingest path already absorbed all the relevant fields).
v0.8.0 Complete

Detection depth + FP discipline

May 12, 2026

  • Native in-process dep-CVE scanners across all three ecosystems. Go (internal/scanner/deps/govulncheck/) — uses the upstream golang.org/x/vuln/scan API in-process; inherits the same call-graph reachability filter as the upstream govulncheck binary but without requiring the binary on the user's PATH. PyPI (internal/scanner/deps/pip/) — parses requirements.txt (== pins only; ranges deliberately skipped, matching pip-audit's posture), POSTs each (package, version) to OSV.dev /v1/query, caches responses at ~/.fendix/cache/osv-pypi/<pkg>@<ver>.json with 24h TTL. npm (internal/scanner/deps/npm/) — parses package-lock.json v2/v3 (full transitive tree, dedup on (name,version)), OSV.dev queries against the npm ecosystem, scoped-package aware (@scope/name handled correctly in path parsing + cache keys), 24h cache. All three behind a single --no-native-deps escape hatch. Toolchain bump 1.21 → 1.22 required (x/tools v0.17.0 has a constant-folding bug under modern Go; v1.1.4 of x/vuln is the earliest version that picks up a fixed x/tools, and v1.1.4 needs go 1.22).
  • Reachable XSS taint chains. New SEC-PY_XSS_HTML_SINK finding (CWE-79, HIGH severity, MEDIUM confidence). Three HTML-render sinks detected: Markup(x) / flask.Markup(x) / markupsafe.Markup(x) (bypasses Jinja2 auto-escaping), mark_safe(x) / django.utils.safestring.mark_safe(x) (bypasses Django's {{ }} escaping), render_template_string(x) (Flask/Jinja2 SSTI + reflective XSS when the template body is user-controlled). When intra-function dataflow proves a request source reaches the sink, the finding carries taint_chain + reachable: true — the correlator then applies a second severity bump for correlated-reachable pairs.
  • Reachable command-injection taint chains. Extended existing PY_OS_SYSTEM and PY_SUBPROCESS_SHELL sinks with _collect_taint_chain capture; added new PY_OS_POPEN sink (deprecated form with the same shell-injection surface). Brings the engine's reachable sink categories to 6 across SQLi/SSRF/open-redirect, XSS, and cmd-injection.
  • FP corpus + targeted gates. New scripts/fp-corpus/run.sh runner plus a false-positive catalog: 35 false-positives across 4 distinct root-cause patterns (test fixtures flagged as production findings, header/CORS check fires on 4xx, rate-limit check on static-file path, metrics-endpoint headers). Two math-shaped levers shipped: 4xx-response gate on header + CORS checks (a missing CSP/HSTS or CORS misconfig on a 404 page isn't actionable — early-return) and static-file path regex on rate-limit check (skips .DS_Store, favicon.ico, robots.txt, .css, .js, .map, .woff2, etc. before sending any probe requests).
  • One-click suppression snippet in PR comments. Every top finding in the GitHub-App-posted PR comment now ships with a fenced yaml block under the bullet — copy-paste straight into .fendix-ignore to suppress. Keyed on a stable (title, category, endpoint) SHA-256 hash (truncated to 8 hex chars in a trailing # fp-<hash> comment) so the suppression survives SEC-NNN reassignment across scans. Addresses the dominant FP corpus pattern: 31 of 35 FPs come from test fixtures being flagged as production findings; the snippet lets a developer suppress the whole cluster in one paste.
  • Severity scoring refresh — reachable_code multiplier first-class. New ReachableMult = 1.5 constant in internal/models/scoring.go + new CalculateSeverityReachable() function (existing CalculateSeverity is a backwards-compatible wrapper). A new orchestrator step bumps severity by one level on pure-whitebox findings with reachable: true (the correlator already bumps the correlated-reachable case in mergeFindings). The existing confidence cap still applies — the bump amplifies what the confidence allows; it can't override the cap. EPSS / KEV multipliers are explicitly deferred.
  • Cross-repo sync: backend persists taint_chain + reachable + affected_endpoints. The engine had been emitting these reachability fields on Finding since v0.7.0, but the backend's _finding_defaults silently stripped them on ingest. New migration 0006_finding_reachability adds 3 columns to ScanFinding (affected_endpoints + taint_chain JSONField, reachable Bool) with safe defaults. _coerce_taint_chain defensive parser drops malformed links rather than failing the whole finding. Frontend Finding interface extended with new TaintLink type + optional taint_chain? / reachable? fields; app/types/api.ts regenerated via npm run codegen. No UI surfacing yet — deferred until a future findings-detail iteration when there's enough reachable-pattern volume to earn its design cost.
  • ADR-008: read-only AI permitted, auto-remediation permanently forbidden. New docs/adr/ADR-008-readonly-ai.md formalises the strategic decision. The boundary: read-only AI (finding explanation + fix suggestion as text, delivered by the cloud product) is permitted and planned. Auto-PR generation, auto-merge, and any LLM calls from the OSS engine binary are permanently forbidden. Rationale covers three options, trust-story constraints, regulatory trajectory, and four constraints on future PRs (no import anthropic in go/ or python/).
v0.7.0 Complete

Open & Extensible — the wedge is now defensible

May 1, 2026

  • The wedge is now defensible. The correlator now distinguishes 'DAST + SAST agreed' from 'DAST + SAST agreed AND we can prove the exploit path' — the latter gets a double severity escalation, which is exactly what makes the wedge defensible against vendor noise. This release closes out the GitHub App business logic and ships open-source ratification, the plugin system, and reachability/dataflow correlation.
  • Open-source posture ratified via ADR-007. What was tactical in v0.1.0 (an MIT LICENSE shipped because the project needed *a* license) is now a deliberate strategic decision: MIT, single repo, no open-core split planned. ADR-007 records the rejected alternatives (Apache 2.0, AGPL 3.0, dual-license, open-core) and why each was the wrong fit. README hero gains a fourth bullet 'Open source under MIT — read the source, audit the wedge, fork it, ship plugins.' CONTRIBUTING.md gains a 'Licensing of contributions' section: by submitting a PR, you agree to MIT for your work; no CLA, no copyright assignment. Out-of-tree plugins choose their own license — only plugins shipped inside this repo (under examples/plugins/) are required to be MIT to match the rest of the tree.
  • Plugin system: out-of-tree extension via NDJSON IPC. New internal/plugin package: Discover walks <repo>/.fendix/plugins/ (repo-local, takes precedence) + ~/.fendix/plugins/ (user-global), parses each child's plugin.yaml with strict KnownFields(true) parsing (typos like entry_point → loud error, not silent drop), and dedups by name. (*Plugin).Run invokes the entrypoint with a JSON ScanRequest on stdin and reads NDJSON Findings on stdout — same wire contract as the embedded Python engine (ADR-002), so plugin authors writing in Python can reuse most of engine.py. Per-plugin timeout (default 30s, max 5m) bounds wall-clock; partial findings before a kill are preserved; plugins inherit FENDIX_PLUGIN_NAME + FENDIX_PLUGIN_DIR env vars; every emitted finding gets fendix-plugin:<name> appended to References for provenance. Plugin findings flow through the same Correlate / Dedup / Sort / ID-assignment pipeline as embedded engine findings — a custom-secret-pattern plugin correlates against blackbox auth checks identically to the built-in secrets analyzer. New `--no-plugins` CLI flag disables discovery for sandboxed CI or debugging. Three reference plugins under examples/plugins/: custom-secret-pattern (Python, regex-based custom secret detection), custom-blackbox-check (Python, custom HTTP-response assertion), custom-semgrep-pack (shell, wraps a custom Semgrep rule pack). Author guide: docs/plugins.md covers discovery, IPC schema, security model, and an authoring checklist.
  • Reachability/dataflow correlation: `correlated:reachable` for proven exploit paths. The Python AST analyzer now records taint chains for SQLi, SSRF, and open-redirect findings: when _collect_taint_chain proves intra-function dataflow from a request source (request.args/POST/form/data/json/headers plus the Flask handler-arg form req) through one or more variable assignments to a dangerous sink, the emitted finding carries taint_chain: [{file, line, expr}, …] plus reachable: true. The chain walks recursively through scope assignments — q = request.args.get('q'); sql = '...' + q; cursor.execute(sql) resolves three links without false positives on literal-only chains. Correlator escalation: when the whitebox half of a correlated pair carries a chain, the merged finding inherits the chain plus reachable: true AND gets a *second* severity escalation. So MEDIUM blackbox + MEDIUM whitebox + reachable jumps to CRITICAL (vs. HIGH without reachability). HTML reporter renders the chain as an ordered list under finding details ('Reachable dataflow (N steps)').
  • GitHub App business logic wired end-to-end. Replaced the v0.6.1 scaffold's stub handlers with the full PR workflow on top of the credentials/auth layer. On every pull_request.{opened,synchronize,reopened}, the webhook handler now: (1) fetches an installation token from the cached TokenSource; (2) clones the PR head SHA via git init + shallow fetch --depth=1 origin <sha> + checkout FETCH_HEAD (only the exact commit, no history; auth via x-access-token:<token>@… userinfo on the HTTPS clone URL); (3) runs fendix scan --code <tmp> --format json; (4) re-renders SARIF via fendix report --format sarif so the PR comment + Code Scanning tab describe identical findings; (5) renders a Markdown PR comment matching the examples/github-actions/fendix-scan.yml github-script template byte-for-byte modulo whitespace; (6) POSTs the comment to /repos/{o}/{r}/issues/{n}/comments; (7) gzip+base64-encodes the SARIF and uploads to /repos/{o}/{r}/code-scanning/sarifs against refs/pull/<n>/head. SARIF upload is best-effort (Code Scanning disabled or security_events: write missing → log warning, comment still posts). `check_run.action == "rerequested"` re-runs the scan against the recorded head SHA. Tempdir always cleaned up via defer os.RemoveAll. Tokens redacted from any error surfaced for git-step failures. Per-scan timeout: 15 minutes wall-clock. Distribution: new Dockerfile.app (multi-stage; bundles fendix + fendix-app + Python engine + git + tini in ~250 MiB Debian-slim). 26 new ghapp tests under -race.
  • Vulnerable-app benchmark numbers captured. Stock fendix scan --url http://localhost:3000 against bkimminich/juice-shop:v17.1.1 on Fendix v0.6.1 produced 97 endpoints discovered / 7 deduped findings (4 MEDIUM + 2 LOW + 1 INFO) / 41.5s scan time / 0 correlated (passive-only run; juice-shop's intentional SQLi/XSS/IDOR vulns need --enable-active and/or --code to surface). Pre-dedup the engine emitted 391 raw findings that deduplication collapsed to 7. Numbers published in docs/benchmarks.md 'Latest results' table with a 'Reading the row' explanation that documents what the row does NOT measure.
  • GitHub App scaffold. New cmd/fendix-app binary (separate from the fendix CLI; long-running webhook server) plus internal/ghapp package and app/manifest.yml for one-click App registration via GitHub's manifest flow. Webhook layer: HMAC-SHA256 signature verification (legacy sha1= rejected), event router, 4 MiB body cap. Auth layer: pure-stdlib RS256 App-JWT signing (no golang-jwt dep added — preserves the project's zero-runtime-deps posture), /app/installations/{id}/access_tokens exchange, single-flight installation-token cache. Setup guide: docs/github-app.md. 28 unit tests under -race. Marketplace listing is an operator step distinct from the code deliverable.
  • `fendix demo` command. New cobra subcommand spins up bkimminich/juice-shop:v17.1.1 in Docker on localhost:3000, runs a stock scan, renders an HTML report, and (with --open) opens it in the user's default browser. Container always cleaned up on exit. Flags: --open, --port, --output, --image. Removes the cold-start 'what does a real scan look like?' question for first-time evaluators.
  • `.fendix.yaml` repo-committed policy. New internal/policy package + new --config <path> flag on fendix scan. Teams commit a .fendix.yaml at repo root encoding scan posture (severity threshold, scan budgets, auth profile reference, crawler defaults, format) and invoke fendix scan with one CLI flag instead of the prior six-flag-and-growing wall. Precedence matches git config: cobra defaults < .fendix.yaml values < explicit CLI flags. Strict YAML parsing (yaml.KnownFields(true)) rejects typos. Schema versioned; future v2 forward-rejected. `fendix init` now writes 3 files (workflow + .fendix.yaml + .fendix-ignore).
  • Backend not extended for plugins, `--config`, or the GitHub App at v0.7.0 release time. Plugins run on the host filesystem (backend container can't see them); --config is a host-filesystem flag (the API itself accepts every policy field directly); fendix-app is a separate deployable on the GitHub-event side of the pipeline.
  • Reachability fields persisted backend-side and typed frontend-side. v0.7.0 originally landed reachable + taint_chain + affected_endpoints on the engine's Finding JSON output, but the backend's _finding_defaults silently stripped them on ingest, so GET /api/findings served lossy rows. Closed in a follow-up: backend migration 0006_finding_reachability adds 3 columns to ScanFinding (affected_endpoints + taint_chain JSONField, reachable Bool) with safe defaults; _coerce_taint_chain defensive parser drops malformed links rather than failing the whole finding; serializer exposes all three; openapi.json regenerated; 3 new tests. Frontend gains a TaintLink interface and Finding.taint_chain? + Finding.reachable?; app/types/api.ts regenerated via npm run codegen. No UI surfacing yet — deferred until the engine ships XSS and command-injection reachability patterns; a real findings-detail iteration earns its design cost against 5 reachable categories, not 1.

v3.7.0 shipped October 2026 — the GitHub Action requires Semgrep for code scans and fails the job when it cannot run, installs wheels only and uploads SARIF only for a finished scan, and a fatal Semgrep run is recorded as failed; no report-schema, SARIF, fingerprint or decision-policy changes, and every exit code keeps its meaning. v3.6.5 shipped October 2026 — Semgrep 1.182.1, MCP 1.29.0 and golang.org/x/mod 0.40.0 fix the last fixable HIGH advisories in the official image, Semgrep no longer contacts semgrep.dev, and self-hosted Linux CI runners need glibc 2.34 or later for Semgrep; no scanner, fingerprint, report-schema, decision-policy or exit-code changes. v3.6.4 shipped October 2026 — rebuilt on Go 1.27.3 and certifi 2026.10.23, so TLS no longer trusts the eight compromised emSign intermediate CAs and a hostile HTTP/2 scan target can no longer burn the scanner's CPU; no scanner, fingerprint, report-schema, decision-policy or exit-code changes. v3.6.3 shipped October 2026 — four bundled Semgrep rules that never matched now report, WARN by default under the confidence-gated --fail-on; the Flask auth advice says the decorator goes below @app.route(...); eleven checks have static catalog copy; no finding-identity change. v3.6.2 shipped October 2026 — diff-aware scans with a relative --code scan the changed files again, and a diff scope that cannot be represented safely falls back to a full scan. v3.6.1 shipped October 2026 — SARIF for a scan with no findings is valid, and the GitHub Action verifies the release signature before it runs the binary. v3.6.0 shipped October 2026 — baselines, .fendix-ignore rules and SARIF act on each security occurrence, so a baselined or ignored test fixture can no longer hide a new production credential grouped with it; baseline format v2, one SARIF result per occurrence, and fendix jira and fendix verify track every occurrence. v3.5.1 shipped October 2026 — fendix init writes CI that installs a pinned, signature-verified Fendix, brew upgrade reaches installs older Homebrew recorded as version 64, and the installer can require a signature or a pinned SHA-256; no scanner, fingerprint, report-schema, decision-policy or exit-code changes. v3.5.0 shipped October 2026 — --fail-on refuses an invalid threshold instead of silently scanning with none, fendix verify no longer reads missing input as a fix, and images publish to Docker Hub; no fingerprint, report-schema or decision-policy changes. v3.4.1 shipped September 2026 — the engine images now ship Go 1.27, raising govulncheck's module ceiling from 1.25; no scanner, fingerprint or exit-code changes, and the Go module floor stays 1.25. v3.4.0 shipped September 2026 — a scan can no longer look complete when it wasn't: every analyzer records whether it ran and why not, the report carries a coverage block that names the gaps, a URL scan with zero endpoints writes its report instead of vanishing, and two opt-in flags turn a coverage gap into a failing build. v3.3.0 shipped September 2026 — an upgrade no longer splits a finding you were already tracking into a closed one and a new one, and the list of checks a scan ran is built from what completed rather than what was configured. v3.2.0 — every blocking decision explains itself. A BLOCK now carries the policy it was made under and the evidence class that justified it, all the way through Fendix's own storage into the SARIF you export, so a failed pipeline is answerable from the report alone. Findings held back say why, confidence scores come with the rules that produced them, and a build blocked only because confidence enforcement was switched off is labelled as such. v3.1.0 stopped findings claiming more than they proved — wildcard CORS with credentials, rate-limit prioritisation, response-aware header grading, path-traversal wording, test-fixture credentials and dependency applicability all became more precise. v3.0 gave findings a stable identity based on which rule fired, in which file, in which function, about which operation. Re-save your baselines once after upgrading from before v3.0, and rewrite any .fendix-ignore rule that pins a fingerprint: value — rules using the supported id, endpoint or category selectors are unaffected by the fingerprint change, although positional SEC-NNN ids are suitable only for short-lived triage.