Skip to content

What's new

A version-by-version history of Fendix development. 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 — 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 — 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 state why they were held back, 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 rule, file, symbol and operation rather than line number; re-save your baselines once after upgrading from before it.

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. All three say the engine catches what it claims at the latency the benchmark publishes on the breadth real-world codebases need. 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 could be SPA-fallback responses — still a real security issue (cache poisoning + WAF confusion).
  • 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. Every OWASP Top 10 category PyGoat advertises was detected.
  • 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 (still a real issue, but remediation may be server-config not secret-rotation). 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.
v0.6.1 Complete

Patch — install.sh `mkdir -p` fix

May 1, 2026

  • Critical install-pipe fix. scripts/install.sh now mkdir -p the install directory before the mv. Previously, curl -fsSL https://get.fendix.dev/install.sh | FENDIX_DIR=$HOME/.local/bin sh failed on any system where $HOME/.local/bin didn't pre-exist with mv: cannot move 'fendix' to '/home/runner/.local/bin/fendix': No such file or directory. Blocked the benchmark CI on every run and broke any first-time user who set FENDIX_DIR to a non-existent dir. The fix tries mkdir -p non-sudo first, falling back to sudo mkdir -p only when a parent up the chain isn't writable — POSIX-sh clean. Mirrored to get.fendix.dev/install.sh automatically by the release pipeline's mirror-sync job
  • `fendix init` zero-config workflow generator. New fendix init subcommand detects the project's stack (Go via go.mod, Python via pyproject.toml/requirements.txt/setup.py/Pipfile, Node.js, Ruby, Rust, Java/Kotlin, PHP) plus a colocated OpenAPI/Swagger spec at any of 14 conventional paths. Writes .github/workflows/fendix.yml (drop-in PR-gated DAST + SAST scan, embedded via go:embed) plus .fendix-ignore (commented starter for finding-level suppressions). Refuses to overwrite by default; --force overrides; --print dry-runs to stdout. Note: fendix init was later extended to also write .fendix.yaml — see the v0.7.0 entry
  • README repositioned around correlated evidence. Hero moved beyond generic scanner positioning to explain how Fendix combines DAST and SAST evidence in one PR check. The release also added clearer trust signals for the single binary, signed releases, and telemetry-free execution.
  • 'What Fendix sends to the network' section at top of README. Five-row table covering default scan / active probing / white-box / no-flags / telemetry. Explicit 'no telemetry code; verify with tcpdump' claim. Plus a 'Verifying signed releases' section with the full cosign keyless verify recipe
  • Vulnerable-app benchmark scaffold. New scripts/benchmark/run-juice-shop.sh + make benchmark + .github/workflows/benchmark.yml (workflow_dispatch only) + docs/benchmarks.md. CI workflow installs Fendix via https://get.fendix.dev/install.sh (doubles as install-pipe smoke test). Real juice-shop numbers captured in the v0.7.0 entry

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 matching by path, category or rule id are unaffected.