In 2006 I started a project called OWASP Orizon because I wanted something that could help me review other people’s source code for security issues. Looking at it from today’s perspective, calling it a static analyzer is perfectly reasonable, and that is how I described the project many times myself. The archived repository still tells the story quite plainly: Orizon was born because I wanted a tool that could parse Java source code, build an Abstract Syntax Tree and use that representation to look for unsafe behaviour. OWASP Orizon on GitHub

What strikes me now, almost twenty years later, is that vulnerability detection itself was never the part of the problem that interested me most. Identifying the use of a dangerous API can be useful, but even in the early versions of Orizon I was already moving toward a different problem: building enough of a representation of the program that security analysis could operate on something richer than text.

That distinction explains a surprising amount of my open-source work since then.

Orizon was already trying to model the program

The architecture grew considerably during the first years of Orizon. By 2008, language-specific translators were expected to parse the source and expose different views of the application to the rest of the engine. The old architecture contained interfaces like these:

public abstract boolean callGraphService(String in, String out);
public abstract boolean dataFlowService(String in, String out);
public abstract boolean controlFlowService(String in, String out);
public abstract boolean designService(String in, String out);

The implementation is obviously a product of its time. XML was used extensively as an intermediate representation, Java compiler APIs provided the AST and the abstractions I would choose today would be very different. The interesting part is not the API design, though. It is the separation that the API was trying to establish.

Parsing the language and describing the program were one problem; applying security reasoning to that representation was another.

The 2008 Orizon architecture described a complete static review as a combination of local control-flow analysis, global call-graph analysis, taint propagation and statistics. It also explicitly described building ASTs before generating representations for variable tracking, control flow and the global call graph. That was a rather ambitious design for a spare-time open-source project, perhaps more ambitious than I understood at the time, but the underlying idea has aged better than much of the implementation.

The OWASP Orizon Framework architecture

By 2010 I had isolated the modeling problem even further. Mirage had become the part of Orizon I cared about most, and in a short project update I wrote something that I had completely forgotten until recently:

“Without a good modeling engine a static analysis tool doesn’t make sense.”

Modeling thoughts before the change

That statement matters to me today because it makes the continuity difficult to dismiss as hindsight. I was explicitly treating source-code modeling as the prerequisite for useful security analysis sixteen years ago.

A few months later I was considering rewriting Mirage around ANTLR and separating it from Orizon altogether. The reasoning was that a multi-language application modeler could be useful independently from security analysis. I wrote that when a reliable source code modeler existed, implementing a security scanning engine over it would become a comparatively affordable task.

Getting ready for a radical change

I would phrase that differently today. Security analysis does not magically become easy once parsing and modeling are solved, because the model itself depends heavily on the security questions you want to answer. Nevertheless, the architectural instinct was already there: the interesting layer was not the collection of vulnerability signatures, but the representation that allowed the engine to reason about them.

Dawnscanner deliberately chose a different trade-off

A few years later, Dawnscanner went in a much more pragmatic direction. Instead of trying to solve general source-code modeling, I concentrated on Ruby applications, the frameworks developers were actually using and a security knowledge base that could be continuously extended.

The README still describes that very concrete scope: detect the Ruby and framework environment, apply checks appropriate to Rails, Sinatra or Padrino, inspect dependencies and gradually improve the analysis of application code itself.

Dawnscanner README

The engine reflects the same philosophy. Once the knowledge base has been loaded, its core job is deliberately straightforward:

@checks.each do |check|
  if checks_to_be_skipped.include?(check.name)
    $logger.info("skipping security check #{check.name}")
  else
    _do_apply(check)
  end
end

Dawnscanner engine.rb

There is something important in that simplicity. Dawnscanner taught me a lesson that the more architectural Orizon never really could: developers do not adopt an architecture, they adopt a tool that does something useful for them.

The knowledge base had to be maintainable. Checks needed to represent real vulnerabilities. Installation mattered, reporting mattered, false positives mattered, and integration with the way Ruby applications were actually built mattered. Over time Dawn accumulated hundreds of security checks and became a real tool rather than an experiment in program analysis.

In 2016, when I briefly returned to Orizon, I actually described that connection myself. I wrote that after successfully starting a similar tool for Ruby, Dawnscanner, I wanted to bring Orizon back to the community. What I did not appreciate at the time was that the two projects had taught me almost opposite lessons about the same field.

This is a story of an endless love — 2016

Orizon made me care about the model underneath the analysis. Dawnscanner made me care about whether the resulting tool was actually useful to the person reviewing the software.

For years I treated those as separate chapters.

DRSource is bringing the two problems back together

DRSource currently describes itself as an extensible, multi-language SAST tool. That is an accurate description of what the repository contains today: regex-based rules coexist with AST-based analysis, there is a configurable knowledge base, and Java, Python and JavaScript/TypeScript analyzers implement increasingly sophisticated forms of data-flow tracking.

DRSource README

What interests me about the project, however, is increasingly the machinery underneath those features.

One of the small but important pieces is a project-wide index used by the taint analyzers:

@dataclass
class FunctionDefinition:
    name: str
    file_path: str
    node: Any
    language: str


class ProjectIndex:
    """
    A global index of all functions and classes discovered across the project.
    Used for inter-file taint analysis.
    """

    def __init__(self):
        self.functions: Dict[str, FunctionDefinition] = {}

    def find_function(self, name: str) -> Optional[FunctionDefinition]:
        return self.functions.get(name)

DRSource ProjectIndex

There is nothing particularly sophisticated about that class yet, and that qualification is important. Proper cross-file analysis eventually requires much stronger symbol resolution, namespaces, classes, overload handling and a much richer understanding of call relationships than a dictionary keyed by a function name can provide.

Conceptually, though, this is where the project begins to become interesting to me.

When the Java analyzer encounters a call that it cannot resolve locally, it can ask the project index for a definition elsewhere in the codebase and continue the analysis inside that function:

func_def = self.functions.get(method_name)

if not func_def and self.project_index and self.depth < self.max_depth:
    global_def = self.project_index.find_function(method_name)

    if global_def and global_def.language == "java":
        func_def = global_def.node["node"]
        target_file = global_def.file_path
        target_code = global_def.node["code"]

        self._simulate_call(
            node,
            func_def,
            method_name,
            target_file,
            target_code,
        )

The simulated call can then map tainted arguments to parameters in the called function and continue walking its body:

if taint:
    loc = (
        f"in {os.path.basename(target_file)}"
        if target_file
        else "locally"
    )

    tainted_params[p_name] = {
        "source": taint["source"],
        "trace": taint["trace"] + [
            f"Passed to {method_name}() {loc}"
        ],
    }

...

if tainted_params:
    body = func_node.child_by_field_name("body")

    if body:
        v = TaintVisitor(
            list(self.sources),
            [{"name": n, "args": a} for n, a in self.sinks.items()],
            list(self.sanitizers),
            t_code,
            self.project_index,
            self.depth + 1,
            initial_scope=tainted_params,
        )

        v.visit(body)
        self.vulnerabilities.extend(v.vulnerabilities)

DRSource Java taint visitor

This is still an early implementation, but the direction is much closer to the problem that originally interested me in Orizon than to simply adding another vulnerability signature. DRSource already tracks tainted values across files, distinguishes object fields, propagates constants, recognises sanitizers and incorporates framework-specific knowledge about sources and sinks.

Those capabilities are useful individually, but I see them primarily as pieces of a program model.

Detecting a dangerous API is not enough

Consider SQL injection. Finding a call to a database execution API is straightforward, but during a real security review that is rarely the question I am trying to answer.

I need to establish whether attacker-controlled data can reach that call.

That means finding the input boundary and following the value through the application. It may enter through an HTTP request, become an argument to a controller method, be copied into an object, pass into a service, cross another file boundary and eventually be combined into a query inside a repository class. Along the way I need to know whether the value was replaced by a constant, constrained by a type conversion, processed by a sanitizer or otherwise transformed in a way that changes the security conclusion.

A human code reviewer reconstructs that context continuously. We jump between definitions, follow variables and fields, recognise framework conventions and keep a partial model of the execution path in our head while deciding which branches deserve more attention.

This is why I increasingly find the traditional concept of a SAST “finding” too small.

The fact that a dangerous function appears at a particular line is evidence, but it is not the analysis. The useful object is the path that connects a security-relevant input to that operation and explains what happened in between.

For DRSource, that distinction has consequences for the architecture. If the useful output is a path rather than a match, then symbol resolution matters more. Calls and returns matter more. Field tracking matters more. Framework semantics matter more. Sanitizers and barriers become part of the evidence rather than exceptions sprinkled into individual rules.

The tool needs to know enough about the program to reconstruct that path with a level of confidence that makes the result useful to somebody who understands the code.

That is what I mean by code understanding.

“Understanding code” does not require pretending the machine is human

The expression deserves some precision because “understanding code” has become rather overloaded, particularly now that almost every developer tool is being connected to a language model.

I am not claiming that DRSource understands a program in the human sense, nor do I think that placing an LLM in front of a repository removes the need for program analysis.

For the security problems I care about, understanding means constructing an explicit model that preserves the relationships required to answer a question. Depending on the analysis, that model might need symbols, scopes, calls, arguments, return values, object fields, sources, sinks, sanitizers, control-flow information and framework semantics.

It will always be incomplete. Static analysis has always involved approximating program behaviour without executing every possible path through the program.

The useful engineering question is therefore not whether the analyzer can completely understand the software. It is whether its representation is precise enough to answer a particular security question while introducing an acceptable amount of noise.

That is a much more interesting problem to me than counting the number of vulnerability classes supported by the scanner.

Twenty years did not make this a twenty-year project

Looking backwards creates a dangerous temptation to turn a sequence of experiments into a carefully planned journey. That is not what happened here.

There was no twenty-year roadmap.

Orizon changed direction several times and eventually stopped. Dawnscanner was built with very different priorities. My day jobs changed, my interests moved through other areas of security and software engineering, and there were long periods when static analysis was not the project occupying my spare time.

I even wrote about this lack of linearity at the time. In 2010 I admitted that maintaining a detailed roadmap for a spare-time open-source project was largely pointless because professional work, family and other projects inevitably competed for the same time.

Getting ready for a radical change

The continuity is therefore not in the repositories.

It is in the problem.

In 2006, Orizon started by parsing source code into a representation that a security engine could consume. By 2010, I was explicitly treating the source-code modeler as the prerequisite for the analyzer I wanted to build.

Dawnscanner then taught me to narrow the scope, encode useful security knowledge and build something that developers could actually run.

Today DRSource is putting those two experiences in the same place. The project is still young and its program model is nowhere near as complete as I want it to become, but I now have a much clearer idea of what I want to optimize for.

I do not want DRSource to become interesting because it reports more findings than another scanner. I want it to become interesting because it can explain increasingly non-trivial security paths through real applications, and because the explanation contains enough evidence for a reviewer to decide whether the path matters.

Reading my own notes from 2010 made me realise that this is not a new obsession.

I have apparently been trying to build some version of this engine since 2006. The difference is that twenty years of writing scanners, reviewing code and watching developers deal with security tooling have made the target considerably clearer.

The hard problem was never spotting the dangerous function.

It was building enough of the program around it to understand why it was dangerous.