Most of our customers, by the time they come to Autobahn Security, they’re usually already running one or more vulnerability scanners, cloud security tools, or specialized platforms. And each tool produces its own stream of findings, in its own format, with its own way of scoring severity. The result is: scattered data, duplicate issues, and no single, trustworthy view of what actually needs attention first.
One of our clients use Aqua Security, and they wanted their scan reports inside Autobahn Security too, aggregated and prioritized alongside everything else. In this post, we sat down with Wisma Ekawati, the engineer behind the Aqua integration, to find out what it took to make it work.
What was the client actually trying to solve, and how did this feature land on your desk?
The client was already using Aqua Security across their containers and getting useful findings from it. The problem was that those findings stayed in Aqua, separate from everything else they were tracking in Autobahn Security. So their engineers had two places to look and no single view of what to fix first.
The request was pretty straightforward: bring the Aqua findings into Autobahn Security, show them alongside the rest of the issues, and prioritize everything on the same scale.
But we didn’t start building straight away. We first treated it as a spike, a small piece of exploratory work to figure out whether the integration was feasible, how the Aqua data would map into our model, and roughly what it would take to build. Only after that did we start working on the connector itself.
When did you know you wouldn’t have direct access to Aqua Security and would be working with exported report files instead? Did that change your approach?
That was a constraint from the start, so it wasn’t something that caught us by surprise halfway through. The client exports the report themselves and uploads the file to Autobahn Security. We never access their Aqua instance directly.
Honestly, that’s less unusual than it might sound. We already have file-upload connectors for other tools, so the basic pattern was there.
What changes is how you deal with the data. With an API, if something looks odd, you can query it again or pull more information. With a file upload, the file is the contract. You have to work with what’s in there, and you only see the report formats and versions the client actually sends you.
What does an Aqua Security report file actually look like, and how did you get familiar with it without being able to generate one yourselves?
It’s a CSV. The one we worked with had 5,003 rows and 24 columns. Each row represents a package-level finding for a particular container on a particular node, so the same advisory can appear many times. Once you break those 5,003 rows down, though, you’re actually looking at just 13 assets and 116 distinct advisories, with 495 relationships between them.
The columns cover three things: the container context (things like the node, cluster, namespace, deployment, and image) the finding itself, including the advisory, severity, fix version, affected package, and package path, and then several different dates showing when something was first found.
As we couldn’t generate reports ourselves, we got familiar with the format by going through the real client export column by column and counting what was actually there. Before we wrote any mapping code, we noted down what we expected to get out of it: how many assets, how many issues, how many had a CVE identifier, and so on. Then we used those numbers to check that our mapping was doing what we thought it was doing.
How did you figure out how to reliably parse that file without being able to produce your own test reports?
Two rules. First, don’t guess at column names but pin them. All 24 header names live in one place and are matched exactly, because a header off by a single character silently produces empty values rather than an error. That is the kind of bug that reaches production looking like “the data is just missing”.
Second, don’t require all 24 columns. We identified five that the import genuinely cannot work without: Name, Image, Advisory name, Severity, and Affected package.
Everything else is optional. A client who adds a column, reorders them, or exports a slightly different profile still imports fine. The parser also tolerates a metadata block above the header row, so an export that doesn’t start on line one still works.
What role did the client play in getting this right? Did they send you sample reports, screenshots, or walk you through their export process?
They gave us a real Aqua export, and that file became the reference for everything. No screenshots, no walkthrough of their export process, just the actual report, which turned out to be more useful than a description of it would have been. Every mapping decision, every column name, and every number we check in our tests traces back to that one file.
It also set some honest limits on what we could claim. The export had already been filtered to critical findings, so it proved that our handling worked for one severity level, but it couldn’t tell us anything about the other four.
You learn as much from what a real file doesn’t contain as from what it does.
Was there a moment where the report format surprised you? Something inconsistent, undocumented, or just different from what you expected?
Yes, and it was the kind of thing that could have quietly shipped the wrong behavior.
There’s a “Custom Severity” column that’s meant to hold the client’s own override of Aqua’s severity rating. In the real export, though, it was blank in all 5,003 rows. That sounds minor, but it completely changes how the fallback logic has to work.
If you treat that empty value as a real severity, the lookup fails and every issue can end up at the lowest severity. If you treat it as missing, you correctly fall back to Aqua’s own rating. It’s a one-line difference in the code, but the wrong version can be very hard to spot, especially if your test data happens to use low-severity examples where the wrong result and the right result look the same.
Two other things stood out. First, the export had already been filtered to critical findings, so our sample only exercised one of the five severity levels. That meant there was a whole part of the logic the file simply couldn’t validate for us.
Second, 74 rows were identical in every field we were capturing except for the deployment name. It was the same node running the same image in two different deployments. If we hadn’t captured that field, those findings could have quietly collapsed into each other, and the client would have lost data without seeing an error.
How did you design the upload experience itself? What happens if the file doesn’t parse correctly?
From the user’s side, it’s the same flow as our other file-based connectors: on the import screen you choose which tool the report came from (such as Aqua, Ionix, Qualys Cloud Agent) and upload the file. There’s nothing Aqua-specific to learn. That was deliberate. We didn’t want the client to have to figure out a completely new upload process just because the data came from a different tool.
Most of the design work went into what happens when something is wrong with the file. The basic rule was: never just tell the user “Failed” and leave them guessing. If a required column is missing, we tell them exactly which one. If individual rows have problems, we group those errors and show the affected row numbers, so you get one useful message instead of hundreds. And if the file has the right structure but contains no data rows, we say exactly that rather than reporting a misleading column error.
One interesting detail is that the preview you see before confirming the import goes through the same severity logic as the actual import. It would be surprisingly easy to build those separately and end up showing one severity in the preview while storing another.
How did you test the upload and parsing logic without access to Aqua Security to generate fresh, varied report files?
We used two layers of testing. The fast tests run on a hand-built example row that’s shaped like the real file, with all 24 columns present and blank values represented as empty strings, because that’s exactly what our parser produces. If you make the fixture too convenient and simply leave out the empty columns, you can easily hide bugs like the severity issue we talked about earlier.
Then we have a second, opt-in test that runs against a real export. It uses the actual parser, with the same chunk size we use in production, and checks the numbers we’d already worked out from the file: 13 assets, 116 issues, and 495 mappings.
That second test really proved its value. It uncovered a bug in a shared CSV library where the conversion said it was finished before the last rows had actually been written to disk. At that point, only 4,925 of the 5,003 rows were there. The remaining rows appeared about a second later, with no error anywhere.
The tricky part was that the number of missing rows depended on the write buffer, not the overall size of the file. So our smaller test files always passed. That’s exactly the kind of problem you only find when you put real volume through the real code path.
What was it like watching this feature go live? And what was the client’s reaction to finally having their Aqua Security data inside Autobahn?
With a file-upload connector, there isn’t really a dramatic moment where data suddenly starts streaming in. The feature goes live, and then it waits for someone to upload a report. The important part was being able to tell the client, “It’s ready. You can use it now.”
They were happy to have it and said they’d try it as soon as they could. For this kind of feature, that’s actually a pretty good reaction. There were no questions about how to use it because there was nothing new to learn: they could keep using the same Aqua export and the same upload flow they already knew.
Looking back, what are you most proud of, and what would you tell another engineer building an upload-based feature for a platform they can’t access directly?
What I’d point at isn’t a feature, it’s a habit: we wrote down the numbers we expected before we built the mapping, and then made a test enforce them. Every real defect we found, the severity fallback, the lost rows, the findings that would have collapsed into each other came out of that, not out of code review.
So make your test data match the real file as closely as possible, including the empty cells, and be very clear about what your sample can and can’t prove. And if the real export doesn’t quite match what the documentation led you to expect, pay attention to the export. That’s the data your parser actually has to handle.