Skip to content
FendixDocumentationDocs

.fendix.yaml · SEC-001 · SARIF

Current release v3.7.0
ArchitectureStablev3.7.0

Architecture

How the Fendix engine combines Go and Python analyzers and evidence collectors, its IPC contract, and the decision inputs.

On this page

System Overview

The Fendix engine combines application-security scanners and evidence collectors. Fendix uses the evidence they produce, together with imported evidence, to evaluate required coverage and produce policy-driven release decisions. Native Go analyzers run from the single binary, and a code scan attempts the Python white-box engine automatically. Standalone release binaries do not bundle the Python tree; the official Docker image does. Use --python-engine to require the engine and make its absence exit 2, or --python-engine=false for the native-only path. The newline-delimited JSON contract (ADR-002) still applies to Python and plugin subprocesses.

architecture
User CLI Command
       |
       v
+----------------------------------+
|            Go Binary             |
|  - CLI (cobra)                   |
|  - HTTP Scanner    (black-box)   |  Sends real HTTP requests
|  - Orchestrator                  |
|  - Correlator                    |
|  - Reporters                     |
|  - Textscan        (native Go)   |  Go / JS / Java / IaC rules
|                                  |
|  In-process Go scanners:         |
|  - Secrets         (TASK-115)    |  18 detectors including ENV_SECRET
|  - 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
       +-------------------+
                 |
                 | --code auto-attempts; explicit flag requires
                 v
       +-------------------+
       |  Python Engine    |  Not bundled in standalone binaries
       |  - Spec Parser    |  Local python/ or FENDIX_ENGINE;
       |  - AST Analyzer   |  Docker ships /opt/fendix/python
       |  - Auth checks    |
       +-------------------+

ADR-001: Go + Python Hybrid Architecture

Accepted (Phase 0); evolved in v0.9 / Phase 17b

Context

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:embed and 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 since v0.9 / Phase 17b — standalone binaries no longer embed Python

ADR-001 remains accepted for the Go/Python split, while its delivery model evolved. The Go scanner ecosystem 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 available binary), and drops the embedded Python distribution from standalone releases. Current code scans attempt the external Python engine automatically; --python-engine requires it and turns absence into exit 2. The IPC contract from ADR-002 remains unchanged.

  • Default cold start dropped to ~5.6 ms p50 (was ~7.3 ms on v0.8).
  • Native analyzers need no Python interpreter Standalone code scans can run without Python, record the missing optional engine as a coverage gap, or disable that attempt explicitly.
  • 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 the Python engine is available)
  • Python subprocess startup adds measured latency whenever that engine runs
  • An explicit --python-engine requires a local Python tree and turns absence into exit 2; an unavailable implicit engine is a coverage gap
  • IPC contract documented and tested end-to-end in CI

ADR-002: Newline-Delimited JSON IPC Contract

Accepted

Context

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)

ScanRequest
{
  "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)

Finding
{
  "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)

terminator
{"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 Finalization and Decision Inputs

Each check assigns a discrete intrinsic severity. Eligible correlation, Proven Path, or proven SAST reachability may escalate it, then confidence caps the finalized severity. The policy evaluates that result alongside confidence, evidence, and coverage. With confidence enforcement on, the default, severity alone never produces BLOCK; --enforce-confidence=false restores the legacy severity-only gate.

scoring-model
Finalized severity entering the decision gate

1. The executable check assigns a discrete intrinsic severity.
2. Correlation, Proven Path, or proven SAST reachability may escalate it.
3. Confidence caps the result: LOW <= MEDIUM, MEDIUM <= HIGH.
4. --fail-on compares this finalized severity with the configured threshold.

With confidence enforcement on (the default), severity alone never produces
BLOCK. The confidence band, supporting evidence, coverage state, and policy
remain separate decision inputs. --enforce-confidence=false restores the legacy
severity-only gate.