GCA Architecture Decision Records
Architecture decisions for the Graph-Based Code Analysis (GCA) additions in EPIC MD-8596.
ADR-001: Intra-Procedural Only for CFG/DFG Extraction
Status: Accepted
Context: CFG and DFG extraction can operate at two scopes: intra-procedural (within a single function) or inter-procedural (across function boundaries via call sites). Inter-procedural analysis is more precise for whole-program reasoning but introduces quadratic complexity, requires a complete call graph before extraction begins, and is sensitive to dynamic dispatch and higher-order functions common in Python.
Decision: Both CFGExtractor and DFGExtractor operate strictly intra-procedurally, producing one FunctionCFG / FunctionDFG per function or method definition. Cross-function data flow is handled separately through the existing call graph CALLS edges in the graph kernel and through the CPG layer's taint query, which combines per-function DFG with call graph adjacency.
Consequences:
- Extraction remains O(n) in function body size, keeping build times linear with codebase size.
- Reaching-definitions analysis uses line ordering as a conservative proxy for control flow, which is correct for straight-line code and safely over-approximates for branches.
- Taint paths that cross function boundaries require composing per-function DFG results with call graph edges, which is less precise than full context-sensitive inter-procedural analysis but avoids the associated scalability wall.
- Nested function definitions are treated as local variable defs; their bodies are not recursed into (separate scope).
ADR-002: CPG as a Composed Overlay (Not a Separate Graph)
Status: Accepted
Context: A Code Property Graph merges AST structure, control flow, and data flow into one queryable representation. The standard approach in tools like Joern creates a dedicated CPG data structure. GraFOSa already maintains a GraphKernel with AST-derived Symbol/File/Endpoint nodes and CALLS/IMPORTS/DEFINES edges.
Decision: CodePropertyGraph is implemented as a composed overlay on top of the existing GraphKernel. It stores CFG BasicBlock nodes and DFG Variable nodes directly into the kernel via add_node/add_edge, using the same node ID namespace. CFG and DFG data are also cached in-memory dictionaries (_cfgs, _dfgs) keyed by symbol FQN for fast analytical queries (taint, slicing) that do not need full graph traversal.
Consequences:
- No graph duplication: CPG nodes coexist with existing AST/call-graph nodes, enabling cross-layer queries (e.g., "which endpoint handlers have taint paths to SQL sinks?") through standard kernel traversal.
- Edge type heterogeneity increases (CONTROL_FLOW, BRANCH_TRUE/FALSE, EXCEPTION_FLOW, REACHES added alongside CALLS, IMPORTS, DEFINES), which motivates the heterogeneous edge weighting system in ADR-005's edge_weights module.
- The
build_from_sourceconvenience method parses source and builds CPGs for all functions in one pass, keeping the API simple for callers like_init_security_analysis.
ADR-003: Thompson Sampling Bandit for Vulnerability Detection Model Selection
Status: Accepted
Context: We have two fundamentally different approaches to vulnerability detection: a DFG-feature-based heuristic model and a rule-based call-graph security slicer. Each has different precision/recall trade-offs that vary by codebase characteristics. Statically choosing one approach leaves performance on the table; A/B testing requires manual intervention.
Decision: Vulnerability detection model selection uses GraFOSa's existing BanditRegistry with Thompson Sampling (Beta-Bernoulli). The register_in_bandit function creates a vulnerability_detection bandit with two arms: dfg_heuristic and rule_slicer. The rule-based slicer receives a warm-start prior (alpha=2.0) reflecting its proven production track record, while the DFG model starts with the default uninformative prior.
Consequences:
- Model selection adapts online: as vulnerability findings are confirmed or rejected, the bandit shifts traffic toward the better-performing approach without manual tuning.
- The warm-start prior means the rule-based slicer is preferred initially, which is the conservative choice for security tooling where false negatives are costly.
- Adding a third detection approach (e.g., a trained GNN) requires only registering a new arm name -- no changes to the selection infrastructure.
- Bandit state is registered during build via
_init_security_analysisand persisted in kernel metadata.
ADR-004: Rule-Based vs ML Vulnerability Detection as Competing Arms
Status: Accepted
Context: The SecuritySlicer uses backward call-graph traversal plus pattern matching against known dangerous/mitigation patterns (e.g., execute + no parameterize = confirmed SQL injection). The VulnerabilityDetector extracts numeric features from DFG structure (taint source/sink counts, reach density, parameter exposure) and applies a weighted scoring model. Both produce vulnerability findings but through fundamentally different mechanisms.
Decision: Both approaches are kept as first-class, independent implementations rather than being merged into a single hybrid detector. The rule-based slicer operates on the call graph and source context patterns (_DANGEROUS_PATTERNS, _MITIGATION_PATTERNS). The DFG heuristic operates on data flow features with a hand-tuned weight vector and threshold (score >= 4.0). They compete as bandit arms (ADR-003), and the CPG layer can optionally combine them: SecuritySlicer.set_cpg() enriches slice results with CPG taint confirmation.
Consequences:
- Each approach can be tested, validated, and improved independently.
- The rule-based slicer excels at known vulnerability patterns with clear mitigation signatures; the DFG heuristic catches novel data flow patterns that rules might miss.
- When both agree (CPG taint confirms a rule-based finding), confidence is boosted, reducing false positive burden.
- The DFG model's weight vector is hand-tuned; replacing it with learned weights from labeled vulnerability data is a future improvement that requires no architectural change.
ADR-005: Graph Simplification Strategies (Chain Collapse, Hub Bypass)
Status: Accepted
Context: Code graphs can have long chains of pass-through nodes (e.g., delegation wrappers, middleware layers) and high-degree hub nodes (e.g., utility modules imported everywhere). Both patterns degrade GNN message-passing effectiveness -- research shows AMPLE's graph simplification reduced average node distance by 41.65%. GraFOSa needs to support downstream ML consumers without destroying structural information needed for precise analysis.
Decision: The simplify module provides two composable strategies that return new GraphKernel instances (never mutating the original): (1) Chain collapse identifies nodes with in-degree=1 and out-degree=1, removes them, and adds SHORTCUT edges with weight equal to the collapsed chain length. (2) Hub bypass adds direct HUB_BYPASS edges between all predecessor-successor pairs of high-degree nodes (degree > configurable threshold, default 10), while preserving the hub node itself. A preserve_labels parameter protects semantically important node types from modification. Both strategies can be applied in sequence via strategy="both".
Consequences:
- Simplification is non-destructive: the original graph is preserved, and the simplified copy retains provenance metadata (
collapsed_count,via_hubproperties on shortcut edges). - Chain collapse is most effective for deeply nested call hierarchies; hub bypass is most effective for utility-heavy codebases. The composable design lets users apply whichever combination suits their graph topology.
- The
compute_avg_distancefunction enables measuring actual distance reduction, making the simplification's impact quantifiable. - Shortcut edges carry weight metadata that the heterogeneous edge weighting system (edge_weights.py) can incorporate into PageRank and blast radius calculations.
ADR-006: Build-Time Security Analysis Auto-Wiring
Status: Accepted
Context: The security analysis pipeline requires multiple components to be initialized in the correct order: source files must be collected, CPGs must be built from source, the SecuritySlicer must be created and enhanced with CPG data, and the BanditRegistry must have vulnerability detection arms registered. Requiring callers to manually wire this is error-prone and leads to partial initialization.
Decision: The _init_security_analysis function in build.py is called automatically at the end of every build_graph execution, after scanning and dependency wiring are complete. It follows a three-phase pattern: (1) collect Python source files from all configured services, filtering out tests/migrations; (2) create a SecuritySlicer and call enhance_with_cpg(source_map) to build CPGs and attach taint analysis; (3) instantiate BanditRegistry and call register_in_bandit to set up the competing detection arms. All three phases are wrapped in exception handlers with degraded-mode fallbacks -- if CPG enhancement fails, the slicer still works without taint data; if bandit registration fails, detection still works without adaptive selection.
Consequences:
- Security analysis is always available after a build with zero additional configuration required from the user.
- The fail-open design (each phase catches its own exceptions and logs warnings) means a failure in ML registration does not block the security slicer, and a CPG failure does not block the overall build.
- Metadata is stored on
kernel.metadata["security_analysis"]with counts of CPG functions built, source files analyzed, and bandit registration status, enabling observability without coupling. - Adding new security analysis phases (e.g., SAST tool integration) follows the same pattern: append a new phase to
_init_security_analysiswith its own exception boundary.