Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building a Rule-Based SAST Engine

Hello! Welcome back.

In our last lesson, we designed the "frontend" of a Static Application Security Testing (SAST) pipeline. We established a process to take raw source code, parse it into an Abstract Syntax Tree (AST), and then construct a Control Flow Graph (CFG) from it. We now have our code represented in a structured, language-agnostic format—our Intermediate Representation (IR).

This lesson addresses the next logical step: architecting the analysis engine that consumes these IRs to find vulnerabilities. We will focus on the following learning outcome: Architect a rule-based and taint analysis engine for SAST, focusing on how rules are applied to the intermediate representation.

We will design the "backend" of our SAST tool by exploring two fundamental analysis techniques:

  1. Rule-based analysis, which is excellent for finding specific, local code patterns.
  2. Taint analysis, a more powerful data-flow technique for tracking how untrusted user input moves through an application.

By the end of this lesson, you will understand the architectural principles behind the engines that power modern SAST tools.

1. The Core of the Engine: A Rule-Based System

At its heart, a SAST analysis engine is a sophisticated system for applying a large set of checks or "rules" to a codebase. Instead of a monolithic, hardcoded block of logic, a well-architected engine treats rules as separate, manageable units. This is a classic application for a rules engine.

Before we dive into the specifics of SAST, let's establish a first-principles understanding of what a rules engine is and why it's a powerful architectural pattern.

Talk (Software - Day 2) - Rules Rule (Creating and Using a Rules Engine)

The talk 'Rules Rule (Creating and Using a Rules Engine)' provides a fantastic, practical introduction to this concept from a software engineer's perspective. It shows how to refactor complex conditional logic into a clean, extensible rules engine.

Please watch the following segments: 01:20 - 07:29: Pay attention to the problem statement (the 'pyramid of doom') and how a rules engine is framed as an alternative computational model, not a heavy 'business' system. 07:29 - 09:24: Focus on the simple, elegant design of the core components: a Rule object (condition + action) and a RulesEngine that runs them against a 'state'. 25:48 - 27:23: This Q&A section discusses the limitations and appropriate use cases, particularly when you have an 'arbitrarily many' number of conditions, which is exactly the case for SAST. 27:23 - 29:43: This part highlights how rules can be defined once and reused, a key principle for building a scalable SAST engine.

The key takeaway is that a rules engine decouples the "what to check" (the rules) from the "how to check" (the engine). In our SAST architecture, the state object passed to the rules is the code's Intermediate Representation (AST, CFG), and the action is to report a vulnerability if the condition is met.

2. Architecting a Syntactic Rule Engine

The most direct way to apply rules is by looking for patterns in the code's structure, as captured by the Abstract Syntax Tree (AST). This is often called syntactic analysis and is effective for finding vulnerabilities that have a clear, local signature.

Examples include:

  • Use of dangerous functions (e.g., eval(), strcpy()).
  • Hardcoded secrets (e.g., password = "...").
  • Incorrect cryptographic API usage.

Let's architect the components needed to build such an engine.

Building Intelligent Code Review Automation: A Deep Dive into Rule ...

The article 'Building Intelligent Code Review Automation' provides a solid architectural blueprint for a rule-based analysis system. It details the key components and their interactions.

Please review the following sections: System Architecture Overview: Study the high-level diagram. Notice how the Rule Engine sits between the Code Parser and the Report Generator. Rule Engine Architecture: Read this section carefully. It shows a Python-like class for the RuleEngine that iterates through a list of rules and evaluates them against an AST node. YAML-Based Configuration: Look at the example security_rules.yaml. This declarative format for defining rules is a critical design choice, allowing security researchers to add new rules without changing the engine's code.

This article gives us our high-level architecture. The engine loads a set of rule definitions and applies them to the AST generated by our frontend. But how does the engine apply a rule to the AST? It can't be random; it needs a systematic way to visit every relevant part of the tree. This is achieved using the Visitor design pattern.

The Understanding Abstract Syntax Trees (AST): How Modern Tools ...

The article 'The Understanding Abstract Syntax Trees (AST)' explains this mechanism perfectly using ESLint as a practical example.

Please read the sections 'AST-Based Rules: How Tools Use It (Example: ESLint)' and 'AST Traversal (Visitor Pattern)'. Focus on how a rule is defined as a function that operates on a specific node type (e.g., BinaryExpression), and how the engine performs a depth-first traversal of the AST, invoking the correct rule functions as it visits each node.

Combining these insights, we can define the architecture of our syntactic rule engine:

  1. Rule Definition Subsystem: Rules are defined in a declarative format (like YAML) specifying the target AST node type, conditions to check, and the vulnerability details to report.
  2. Rule Loader: On startup, the engine parses the YAML files and instantiates Rule objects.
  3. AST Traversal Engine: This component implements the Visitor pattern. It walks the AST provided by our frontend in a depth-first manner.
  4. Rule Dispatcher: For each node visited, the traversal engine looks up the rules that have registered an interest in that node's type (e.g., FunctionCall, StringLiteral) and executes their evaluate method.
  5. Findings Collector: If a rule's condition is met, it creates a Finding object, which is collected and passed to the reporting subsystem.

This architecture is simple, modular, and highly extensible—perfect for a foundational SAST capability.

3. Architecting a Taint Analysis Engine

Syntactic analysis is powerful but limited. It struggles with vulnerabilities that depend on the flow of data across the application. For instance, os.system('ls -l') is safe, but os.system(user_input) is a command injection vulnerability. The function call is the same; what matters is where the data came from.

This is the domain of taint analysis, a form of data-flow analysis. The core idea is:

  • Sources: Mark data coming from untrusted sources (e.g., HTTP request bodies, database results) as "tainted".
  • Sinks: Identify sensitive functions where tainted data could cause harm (e.g., an SQL query executor, an HTML renderer).
  • Propagation: Track the tainted status of data as it's assigned to variables, passed to functions, and concatenated with other data.
  • Vulnerability: A flow of tainted data from a source to a sink without passing through a sanitizer (a function that cleanses the data) is a potential vulnerability.

This requires us to analyze the program's execution flow, making the Control Flow Graph (CFG) from our last lesson essential.

Taint Analysis Process Flow
This diagram illustrates the fundamental stages of a taint analysis engine. It begins with building a data flow graph, propagates the 'taint' from sources, checks if it reaches any sinks, and finally filters the results.

Let's architect an engine based on these four steps:

  1. Build a Graph of Data Flow: While the CFG shows control flow, we need a Data Flow Graph (DFG) to see how data moves. A DFG can be built on top of the CFG and AST. Each node in the DFG represents a program element (variable, function call), and an edge A -> B means data can flow from A to B. For example, in y = x, we would add a DFG edge from x to y. More advanced IRs like the Program Dependence Graph (PDG) make these dependencies explicit.

  2. Propagate Taint: This is the core analysis algorithm.

    • The engine is configured with a list of sources (e.g., flask.request.args.get) and sinks (e.g., sqlalchemy.execute).
    • The analysis starts by marking all data originating from source functions as tainted.
    • It then iterates through the DFG, propagating the taint. If a variable x is tainted and the DFG has an edge x -> y, y also becomes tainted. This process continues until a "fixed point" is reached, where no more new taints can be propagated.
  3. Check for Paths to Sinks: After propagation is complete, the engine checks if any variable passed to a sink function is tainted.

  4. Filter Out False Positives: The engine checks if the tainted data passed through a known sanitizer function (e.g., an HTML escaping function) on its path to the sink. If so, the finding is suppressed.

This architecture is significantly more complex than the syntactic engine but can find much more subtle and dangerous vulnerabilities.

4. A Modern Hybrid Architecture: Deterministic Analysis + AI Filtering

Taint analysis is powerful, but notoriously prone to false positives. A path for data might exist in the graph, but it may not be feasible in a real execution. Architecting a system to deal with this noise is a major engineering challenge for SAST products.

Modern approaches are starting to combine traditional, deterministic analysis (like CodeQL's taint analysis) with probabilistic models like Large Language Models (LLMs) to triage and prioritize findings.

Scaling LLM-Based Vulnerability Research via Static Analysis and Document Ranking

The video 'Scaling LLM-Based Vulnerability Research via Static Analysis' demonstrates a cutting-edge architecture for this. The presenter shows a tool called 'slice' that uses a classic SAST engine to generate candidates and then an LLM to find the true positives.

Please watch the segment from 45:01 to 59:35. Focus on the architectural pattern of the 'slice' tool: It runs a very broad, aggressive CodeQL query for use-after-free bugs, which is known to generate many false positives (217 candidates). It uses an LLM in a 'triage' step to filter this down to just the candidates where the data flow is valid (7 candidates). It then uses a more powerful LLM in an 'analyze' step on the filtered set to identify the single, true vulnerability. \nThis showcases a powerful system design for managing the signal-to-noise problem in advanced static analysis.

This hybrid architecture represents the state of the art. It leverages the strengths of both worlds:

  • Deterministic SAST Engine (e.g., CodeQL): Provides high recall by exhaustively searching for all potential data flow paths. It does the heavy lifting of graph building and pathfinding at scale.
  • AI/LLM-based Filter: Provides high precision by applying semantic understanding to the small set of candidates, filtering out false positives and ranking the real vulnerabilities by likely impact.

This allows security teams to focus their limited attention on the most critical issues, a key value proposition for any security product.

Conclusion

In this lesson, we have designed the core analysis backend for a SAST tool. We moved from the IRs we created in the last lesson to a complete engine capable of finding vulnerabilities.

Key Takeaways:

  • A SAST analysis backend is fundamentally a rule engine that operates on an Intermediate Representation (AST, CFG, DFG) of the source code.
  • Syntactic rule engines use patterns like the Visitor pattern to traverse the AST and find local, structural anti-patterns. They are simple to build and effective for certain bug classes.
  • Taint analysis engines are more complex data-flow systems. They build a data flow graph to track untrusted data from sources to sinks, enabling the detection of injection-style vulnerabilities.
  • A key architectural challenge in SAST is managing the signal-to-noise ratio. Modern systems may use a hybrid approach, combining a high-recall deterministic engine with a high-precision AI-based filter to surface the most critical findings.

Preview of the Next Lesson:

We have now designed a complete SAST tool, which analyzes code "statically" without running it. However, some vulnerabilities only manifest at runtime. In our next lesson, we will begin exploring the world of Dynamic Application Security Testing (DAST) by designing a system that interacts with a live, running application to find bugs. We'll start by addressing the outcome: "Design a DAST crawling engine capable of link discovery, SPA handling, and authenticated attack surface mapping."

Can't find a good explanation? Sign up and we'll make it for you

Sign up