The JVMXRay Journey: From SecurityManager to Bytecode Injection

,

Every project has a story, not just the polished version you see on a README, but the real one. The wrong turns, the existential threats, the moments where you wonder if the whole thing is dead. JVMXRay has had all of those moments, and it’s still here. This is the story of how it got from there to here.

The Original Idea: Using SecurityManager Sideways

JVMXRay started in early 2020 with a simple but unconventional idea. Java’s SecurityManager had been around for decades, it was designed to enforce security policies, controlling what code could and couldn’t do inside the JVM. Most people used it (if they used it at all) for exactly that purpose. But I saw a different angle.

What if you didn’t use SecurityManager to block anything? What if instead, you used it as a window, a way to observe every access to protected resources in real time? File reads, socket connections, process execution, class loading. The SecurityManager already had hooks into all of it. By implementing a “null” security manager that enforced nothing but logged everything, a Java application’s security relevant behavior could be externalize without changing a single line of its code.

That was the birth of NullSecurityManager. Attach it to any JVM with a command line flag, and suddenly visibility is gained into what that application was actually doing under the hood. No instrumentation. No source code access required. Just observation.

It was a clean idea. Execution, however, was another story.

The Classloader Wars

The first real obstacle was classloaders. If you’ve ever worked deep inside the JVM startup sequence, you know that the boot classloader is a different world. It’s the primordial environment, the JVM is still assembling itself, and the rules are different. Third party libraries? They don’t exist yet. The application classpath hasn’t been set up. You’re running in a stripped down sandbox within a sandbox.

This created immediate problems. JVMXRay needed a logging framework. I chose Logback for its flexibility and its SLF4J compatibility. But Logback is a third party library, and the boot classloader doesn’t know about third party libraries. Attempting to use Logback during early JVM initialization would fail in spectacular and confusing ways. ClassNotFoundException. NoClassDefFoundError. Silent failures where events simply vanished.

The challenge was making JVMXRay’s use of Logback essentially invisible to the host application. The agent’s logging infrastructure couldn’t collide with whatever logging the application itself was using. And it had to work during those early moments of JVM startup when the classloading hierarchy was still being constructed. This took considerable experimentation, namespace isolation, careful ordering of initialization, and later proxy logging before it became reliable.

Early Traction and Community

Despite the growing pains, JVMXRay started gaining attention. The project found a home at OWASP. John Melton contributed the first external pull request in February 2020, just weeks after the initial commit. August Detlefsen followed with significant contributions: restructuring the project to standard Maven layout and later splitting it into a multi module architecture with Docker support. A client-server model began to emerge, with REST endpoints for configuration and event collection.

I presented JVMXRay at Black Hat USA 2020 Arsenal, demonstrating the concept to the security community. The project was covered by, KitPloit, Kali Linux Tutorials, and SecurityOnline. The core idea, using Java’s own security infrastructure to monitor rather than enforce, it resonated with people who understood the gap between application security theory and what actually happens at runtime.

The Deprecation That Almost Killed the Project

Then came JEP 411.

In Java 17, Oracle deprecated SecurityManager for removal. The component that JVMXRay’s entire architecture was built on was being taken away. Not just deprecated in the “we’ll leave it alone” sense, deprecated with intent to remove, which eventually happened in JDK 24.

This was an existential moment. Years of work on the NullSecurityManager, the event capture system, the adaptor architecture, all of it was built on a foundation that was being demolished. I won’t pretend I didn’t consider shelving the project entirely.

But the problem JVMXRay solved hadn’t gone away. Applications still needed runtime security monitoring. Attacks against large supply chains were becoming more common, not less. The need to see exactly what code was actually doing inside the JVM, what files it touched, what connections it made, what processes it spawned, was more relevant than ever. The question wasn’t whether JVMXRay should exist. It was whether it could survive a complete architectural transplant.

The Bytecode Injection Pivot

The answer was bytecode injection.

Before committing to a full rewrite, I developed proof-of-concept code to experiment with the approach. I experimented with AOP and other bytecode injection frameworks. I finally settled on a Java agent with ByteBuddy, a powerful bytecode manipulation library, to intercept method calls at the bytecode level. Instead of relying on SecurityManager callbacks, JVMXRay would weave monitoring logic directly into Java platform classes used by applications as they were loaded.

The POC work was critical. It proved that bytecode injection could replicate what NullSecurityManager had done, and in many ways do it better. ByteBuddy gave us finer grained control over exactly which methods to intercept. We weren’t limited to the SecurityManager’s predefined checkpoint set anymore, we could monitor SQL queries, HTTP requests, cryptographic operations, deserialization patterns, and more.

The SecurityManager was removed. The architecture was reworked from the ground up. JVMXRay was reborn as a Java agent deployed with a single flag and the zero code change philosophy survived intact.

Logback as the Security Event Framework

With the new architecture came a deliberate design decision: make Logback the backbone of the security event system. Rather than inventing a proprietary event format or building custom transport mechanisms, JVMXRay events became simple Logback messages, structured log entries with key/value pairs.

This was a pragmatic choice with far reaching benefits. Organizations already have logging infrastructure like Splunk, ELK, Datadog, and Grafana. By expressing security events as structured log messages, JVMXRay’s output could flow directly into existing SIEM and observability pipelines without custom integration work. In a nutshell, a JVMXRay event is just a log message. Your existing tools already know what to do with log messages.

There’s a deeper point here about structured logging. In most organizations, logging is a free-for-all. A hundred developers across a dozen teams, each writing log messages however they see fit. One developer logs "User logged in: admin". Another logs "LOGIN|user=admin|ip=10.0.0.1". A third logs a JSON blob. A fourth doesn’t log authentication events at all. The result is a haystack of inconsistent, barely parseable text that makes automated analysis, the kind you need for security monitoring, unreliable at best.

JVMXRay sidesteps this problem entirely. Because events are generated by sensors rather than by human developers, every event of a given type has the same structure, every time. A file access event always contains the same fields: the resolved path, the calling class, the operation type, the thread context, the correlation data. A network event always contains the remote host, port, protocol, and TLS status. There’s no variation based on who wrote the code or how they were feeling that day.

This consistency is what makes downstream analysis possible. You can write reliable Splunk queries, build Grafana dashboards, train anomaly detection models, and set up automated alerts — all because the data has a predictable shape. Structured logging isn’t just a nice-to-have. It’s the difference between security data you can act on and security data that just takes up disk space.

The agent uses a shaded copy of Logback under an agent.shadow namespace, keeping its logging completely isolated from whatever the host application uses. That earlier struggle with classloader conflicts? It informed this design. The lessons from those early failures became architectural principles.

The Sensor Architecture

The bytecode injection approach opened the door to a pluggable sensor architecture. Each sensor is a self contained module responsible for monitoring a specific category of JVM behavior. The current lineup includes sensors for:

  • File I/O operations with full path resolution
  • Network connections with TLS detection
  • SQL query execution and database metadata
  • HTTP request and response handling
  • Process execution tracking
  • Cryptographic operations
  • Authentication and session management
  • Object deserialization patterns
  • Reflection-based access
  • Library loading and inventory
  • Script engine usage
  • JVM health metrics
  • Uncaught exceptions and crash diagnostics

Sensors are loaded reflectively, and they are enabled and disabled through configuration without touching code. Each sensor went through multiple iterations of metadata improvement to increase the relevance of captured events. Early versions captured that a file was read; current versions capture the full resolved path, the calling class, the thread context, and correlation data linking it to the broader execution chain.

Event Correlation: Following the Chain

One of the most significant additions came from a question: when a security event fires, what caused it? A file read doesn’t happen in isolation, it’s the result of an HTTP request, which triggered a database query, which loaded a configuration file, which read from disk. Understanding security events means understanding their context.

This led to the development of Mapped Correlation Context (MCC), a thread scoped correlation system inspired by Ceki Gülcü Mapped Diagnostic Context(MDC) in the original log4j distribution. Unfortunately, I couldn’t use MDC feature directly. The MDC uses thread-local variables with no built-in means to pass context to child threads; nevertheless, the concept was very useful. The MCC maintains a trace through sensor activations like so, HTTP>SQL>FileIO. It’s sort of like a stacktrace but for events. Technically, each event carries a trace_id linking related events across an execution context, a scope_chain showing the nested sensor call path, a parent_scope identifying the immediate parent sensor, and a scope_depth indicating nesting level.

The result is that you don’t just see isolated events anymore — you see event chains. An inbound HTTP request triggers a SQL query which triggers a file read which triggers a socket connection. MCC makes that causal chain visible, turning raw events into narratives that security analysts and automated systems can reason about.

Tightening and Optimizing

With the architecture stabilized, the project went through multiple passes of consolidation and optimization. The multi module Maven structure, which had grown to include separate modules for common code, the agent, MCP client, log services, REST services, and AI services, was collapsed into a single module project. Dead code was removed. Dependencies were audited and updated, closing seven CVEs in the process. The codebase migrated to Java 17 as a minimum, and from javax to jakarta namespaces for Spring Boot 3 compatibility.

Memory leak fixes, agent monitoring metrics, and a hybrid file and database logging pipeline replaced the earlier approaches. A primitive ETL pipeline was introduced to promote raw events through processing stages. The Turtle Integration Test was developed to validate minimal sensor operability at build time.

Each optimization pass made the codebase leaner and the architecture clearer. Sometimes the best engineering isn’t adding features, it’s removing everything that doesn’t need to be there.

Where It Stands Today

JVMXRay is currently a POC. It’s a Java agent that you attach to any JVM application with a single command line flag. It uses ByteBuddy to inject monitoring sensors at the bytecode level, captures security relevant events as structured Logback messages, correlates those events into causal chains, and delivers them to whatever logging infrastructure you already have.

It has been a six-year journey from that first commit in February 2020. The project has survived a complete architectural upheaval, classloader nightmares, and the deprecation of its core dependency. It has been presented at Black Hat, adopted by OWASP, and moved out of OWASP to it’s own project when OWASP decided it was going to own all the repo’s that bear it’s projects. That’s tech industry, nothing stands still for long.

The problem JVMXRay set out to solve, making the invisible visible inside the JVM, hasn’t changed. The implementation has been rebuilt from the foundations up. And honestly, painful as it was, it’s better for it. The constraints imposed by the SecurityManager’s deprecation forced a rethink that produced a more flexible, more powerful, and more future proof architecture than the original ever was.

Sometimes the thing that almost kills your project is the thing that makes it worth continuing.

A New Relevance: AI Generated Code and the Velocity Problem

There’s an irony to JVMXRay’s timing. When the project started in 2020, the challenge was monitoring applications written by human developers. In 2026, a growing percentage of production code is being written, or substantially modified, by AI coding assistants. This created a challenge nobody has a good answer for yet.

Code velocity exploded. Engineers using AI tools can produce in hours what used to take days. But review capacity hasn’t kept pace. Engineering teams can’t thoroughly review all the code they’re generating, and security teams, already stretched thin, have even less hope of keeping up. The traditional model of securing software by reading and understanding the source code is breaking down under sheer volume.

This is where JVMXRay’s approach becomes unexpectedly relevant. JVMXRay doesn’t care about code. It doesn’t read source files. It doesn’t parse syntax trees or run static analysis. It watches what an application does, what sockets it opens, what files it reads and writes, what network connections it makes, what processes it spawns, what database queries it executes. The behavioral layer.

It doesn’t matter whether the code was written by a senior engineer, a junior developer, or Claude Code AI. If an application starts making unexpected outbound connections, reading files it shouldn’t be touching, or executing processes that weren’t part of the design, JVMXRay sees it. The code velocity problem is a source code problem. Runtime monitoring operates beneath that entirely.

As AI assisted development accelerates, the gap between code production and code review will only widen. Tools that provide security visibility without requiring someone to read every line of code aren’t just useful — they’re becoming essential.

JVMXRay is open source under the Apache 2.0 license. You can find it at github.com/spoofzu/jvmxray.