Architecture
Architecture Decision Records (ADRs) and the design rationale behind the Fendix hybrid scanner.
Current execution scope
- Runtime
- Prebuilt Go binaries do not require a Go compiler for basic scanning. Building from source requires Go 1.25+. Optional analyzers have their own runtime requirements; inspect coverage to confirm they actually ran.
- DAST
- URL and API probing inspect observable runtime behavior. Intrusive probes require --enable-active. An HTTP 200 response alone does not prove access to sensitive data.
- SAST
- Native static rules cover supported Go, JavaScript, Java and infrastructure patterns. Semgrep and the opt-in Python engine extend applicable checks. Language support does not imply complete framework or path coverage.
- SCA
- Go analysis uses govulncheck. Python and npm dependency scanners use supported manifests and lockfiles, including poetry.lock, Pipfile.lock and package-lock.json. Advisory queries can transmit package names and versions.
- Evidence and policy
- Correlation connects supported matching observations; not every finding has multiple sources. Strong single-source evidence can be sufficient. Severity, confidence, finding disposition and release recommendation are distinct.
- Coverage and human review
- An analyzer failure or missing required evidence can leave the release incomplete. A PASS is a result under the declared policy and coverage, not a guarantee of security or a human approval.
Benchmark tables below are historical, scoped observations. Counts and synthetic regression scores are not production precision/recall. No replacement measurements are claimed here.
Read deployment-specific data handlingSystem Overview
Fendix is a hybrid scanner. As of v0.9 (Phase 17b), the default scan path is a single Go binary — secrets and dependency-CVE checks run in-process; Semgrep shells out to the host binary if installed; the Python whitebox engine for auth / injection / AST checks is opt-in via --python-engine. The cross-engine communication contract (newline-delimited JSON, ADR-002 below) still applies for plugins and the opt-in Python path.
User CLI Command
|
v
+----------------------------------+
| Go Binary |
| - CLI (cobra) |
| - HTTP Scanner (black-box) | Sends real HTTP requests
| - Orchestrator |
| - Correlator |
| - Reporters |
| |
| In-process Go scanners: |
| - Secrets (TASK-115) | 16 patterns + .env handling
| - Dep CVE (TASK-119) | govulncheck / pip / npm
| - Plugin runtime (TASK-113) | NDJSON IPC subprocess
+----------------+-----------------+
|
| shells out to host binary
v
+-------------------+
| semgrep (host) | TASK-116 — graceful absence
+-------------------+
|
| --python-engine OPT-IN only
v
+-------------------+
| Python Engine | Optional — no longer bundled
| - Spec Parser | Requires local python/ tree
| - AST Analyzer | or FENDIX_ENGINE pointing at one
| - Auth checks |
+-------------------+ADR-001: Go + Python Hybrid Architecture
Accepted (Phase 0); evolved in v0.9 / Phase 17bContext
Fendix needs two fundamentally different capabilities: black-box HTTP scanning (high concurrency, low latency, single binary distribution) and white-box static analysis (originally written in Python because the security tooling ecosystem there — Semgrep, Bandit, detect-secrets — was a year ahead of any Go equivalent). No single language excelled at both.
Decision (Phase 0 — v0.1 through v0.8)
- Go for the CLI interface, HTTP scanner, orchestrator, correlator, and report renderer. Compiles to a single binary with excellent concurrency primitives.
- Python for the static analysis engine, embedded in the binary via
//go:embedand extracted to~/.fendix/engine/on first run. Spawned as a subprocess. Carried Semgrep, secrets, AST, and dependency CVE checks. - Communication via newline-delimited JSON over stdin/stdout (see ADR-002 below).
Evolution in v0.9 / Phase 17b — default no longer carries Python
Historical architecture decision: the original ADR-001 was superseded by the native Go scan path. The Go scanner ecosystem has matured (govulncheck has call-graph reachability; the regex story is fine for secrets), and the embedded-Python cost — install footprint, cold-start latency, “do I have Python 3.9+?” support questions — outgrew the benefit. Phase 17b ports the secrets and Semgrep paths to native Go ( internal/scanner/secrets/, internal/scanner/semgrep/ shells out to the host binary), drops the embedded Python distribution from the binary, and makes Python whitebox spawning opt-in via --python-engine. The IPC contract from ADR-002 is preserved for plugins and the opt-in Python path; nothing about the wire shape changed.
- Default cold start dropped to ~5.6 ms p50 (was ~7.3 ms on v0.8).
- No Python interpreter requirement in the default scan path — fendix runs on machines without Python installed.
- Same finding shape across the transition —
SEC-*IDs, severities, references all match byte-for-byte; existing ingest pipelines absorb the new findings unchanged.
Positive Consequences (still hold)
- Best tool for each job — Go for networking, Python remains an option for AST-heavy analysis
- Single binary distribution for the CLI (Python no longer bundled)
- Optional Python engine remains independently runnable for debugging
- Clean separation of concerns between engines via the NDJSON IPC contract (still load-bearing for plugins)
- Each engine can still be tested independently
Trade-offs & Mitigations
- Two language ecosystems to maintain (Go modules + pip when Python is opted in)
- Python subprocess startup adds ~18 ms to opt-in scans (measured)
- Users who relied on the implicit auth/injection Python checks must add
--python-engine+ provide a python tree - IPC contract documented and tested end-to-end in CI
ADR-002: Newline-Delimited JSON IPC Contract
AcceptedContext
The Go orchestrator needs to communicate with the Python engine. The protocol must be simple, debuggable, streamable, and reliable.
Options considered: gRPC (too complex), Unix socket + JSON-RPC (socket management overhead), newline-delimited JSON over stdin/stdout (simplest).
IPC Schema
ScanRequest (Go → Python stdin)
{
"mode": "whitebox",
"spec": "./openapi.yaml",
"code_path": "./src/",
"language": "python",
"checks": ["secrets", "auth", "injection", "semgrep", "deps"],
"verbose": false
}Finding (Python → Go stdout, one per line)
{
"id": "SEC-001",
"title": "Hardcoded API key detected",
"severity": "CRITICAL",
"source": "whitebox",
"category": "secrets",
"endpoint": "src/config.py:14",
"evidence": "API_KEY = [REDACTED len=40 sha256:1f4a9c2e...]",
"fix": "Move to environment variable. Rotate the exposed key immediately.",
"references": ["CWE-798"],
"confidence": "HIGH",
"line": "src/config.py:14"
}Stream terminator (final line)
{"done": true, "total": 12}Positive Consequences
- Zero dependencies — JSON and stdin/stdout exist in every language
- Streamable — findings appear in Go as Python discovers them
- Debuggable — pipe to jq or cat for inspection
- Python engine independently testable
- No network ports, sockets, or connection management
Trade-offs & Mitigations
- No schema validation at protocol level (mitigated by tests)
- No bidirectional communication mid-scan
- Evidence fields truncated to 200 characters maximum
- End-to-end contract tests run in CI
Severity Scoring Model
Every finding is scored based on impact category, detection confidence, and whether multiple detection methods agree (correlated source gets a 1.1x multiplier).
Score = ImpactBase[category] x ConfidenceMult[confidence] x SourceMult[source]
CRITICAL >= 9.0 | ImpactBase: ConfidenceMult: SourceMult:
HIGH >= 7.0 | auth_bypass: 10.0 HIGH: 1.0 correlated: 1.1
MEDIUM >= 4.0 | injection: 9.5 MEDIUM: 0.75 blackbox: 1.0
LOW >= 1.0 | secrets: 9.0 LOW: 0.5 whitebox: 0.9
INFO < 1.0 | idor: 8.5
| data_exposure: 7.0
| cors: 6.5
| headers: 4.0
| info_disclosure: 2.0