Build

Your security scanner cannot read your protected bundle

Someone points a scanner at your production bundle, gets a clean report, and circulates it as evidence. Or gets three hundred findings and circulates that instead. Both outcomes are artefacts of running the tool against the wrong file at the wrong point in the pipeline, and the fix is an ordering rule rather than a configuration change.

Three tools, three different inputs

Security scanning is usually discussed as one activity, which hides the fact that the three common tool families read completely different things. Getting this wrong is what produces the confusing results, so it is worth stating plainly before anything about protection enters the picture.

Static application security testing reads your source and reasons about it: which variable holds user input, where that value travels, and whether it reaches somewhere dangerous. Software composition analysis reads your dependency manifests and lockfiles to work out which third-party packages you use and which of those have known vulnerabilities. Dynamic testing ignores code entirely and exercises the running application over the network.

A transform changes one of those three inputs substantially, one of them not at all, and the third in a way that looks like an improvement and is not. Taking them in that order makes the whole picture straightforward.

Static analysis reads names and strings, and the transform removes both

The reason a static analyser struggles with protected output is not that the file is large or ugly. It is that the analyser’s entire method depends on properties the transform deliberately removes.

Taint analysis works by following a value from a source to a sink, and it recognises both by name. It knows that a particular DOM property is attacker-controlled and that a particular assignment target executes markup, and it tracks the path between them. Rename every identifier to a short generated symbol and the rules that matched on names have nothing left to match. Move every string literal into a table and fetch it through an index expression, and a check looking for a dangerous literal argument now sees a function call whose value it cannot resolve. Flatten control flow into a dispatch loop and the path-sensitive reasoning that made the analysis meaningful is walking a state machine rather than your program.

The result is not a smaller number of real problems. It is a report that has lost the ability to see them, usually accompanied by a scattering of low-confidence noise about dynamic dispatch. Both halves of that report are misleading: the silence reads as assurance and the noise reads as risk, and neither corresponds to anything about your application.

This is the same shape as the minification ordering question. The transform is the last thing that should touch your code, and anything that needs to understand the code has to run before it.

Composition analysis is unaffected, because it never reads your bytes

The tool that teams most often worry about here is the one least affected. Software composition analysis identifies your dependencies from your package manifest and lockfile, which the transform never touches, so its inventory and its vulnerability matching work exactly as before.

This is worth internalising because it removes a common objection to protecting a build at all. Your ability to answer whether you are exposed to a newly disclosed vulnerability in a dependency does not depend on your bundle being readable. It depends on your lockfile, which is in your repository.

The related question of whether a bill of materials survives the process, and what a consumer of your artefact can determine from it, is a genuinely separate topic and is handled in its own article.

Dynamic testing barely notices, and that is its own trap

Dynamic testing drives the running application and observes responses, so a transform makes little difference to the injection, authentication and access-control classes it is looking for. Server-side behaviour is unchanged.

There is one effect worth knowing about, and it points the wrong way. Many dynamic scanners build their attack surface by crawling, and part of crawling is parsing JavaScript to discover endpoints and parameters. Against a protected bundle that discovery works less well, so the tool finds fewer routes, tests fewer things, and reports fewer findings.

A shorter report after a protection change is a coverage regression wearing the costume of an improvement. If you compare scan results across that change, compare the number of routes exercised before comparing the number of findings, because the second number is only meaningful when the first one held steady.

The ordering rule that keeps all three honest

The rule is short: analyse the source, protect the artefact, then record what you produced. Static analysis and code review run against readable source in the repository, before the transform, where their assumptions hold. Composition analysis runs against the manifest, which is unaffected by ordering. Dynamic testing runs against a deployed environment, and if you want it against a protected build then keep the crawl seeded from a route list rather than from discovery so coverage stays comparable.

The transform then runs last, and the integrity records are produced from its output. This is also where a manifest earns its place: it carries a source digest and an output digest for each file, which is precisely the link between the thing you analysed and the thing you shipped. Without that record the two are related by assertion.

One practical note for pipelines that gate on scan results. Because the analysis step now runs on a different file from the one that gets deployed, it is worth making that explicit in the job definition rather than leaving it implicit in the ordering, so that a later change to the pipeline does not silently start scanning the output.

What to hand an auditor who scanned the wrong file

This arrives as a report from someone outside your team, and it is more often the noisy version than the silent one. The productive response is not to argue about the tool.

Explain the input mismatch in a sentence: the scanner was given a build output, its rules depend on identifiers and literals, and the transform rewrites both. Then supply the scan that does mean something, which is the static analysis run against the source revision that produced the artefact. Then close the gap between them with the manifest entry linking that source digest to the output digest they hold, and, if you record a seed, the fact that the build is reproducible from those inputs.

That sequence converts an argument about tooling into a chain of records, which is the same move that works when a penetration test flags the bundle itself. The thing that makes it work is having the records already, which is a build-time decision rather than a response-time one.

What is not worth doing is weakening the protection so an external tool produces a more comfortable report. You would be trading a real property of your shipped artefact for a number that was measuring the wrong file.

The short version

Static analysis depends on names, strings and control flow, and a transform removes all three, so scanning a protected artefact yields silence and noise rather than assurance. Composition analysis reads your lockfile and is unaffected. Dynamic testing is mostly unaffected except that reduced crawl discovery makes coverage fall while the report gets shorter. Analyse the source, protect last, and keep the digests that link the two.

Frequently asked questions

Should we run our SAST scanner before or after obfuscation?

Before, without exception. Static analysis identifies sources and sinks by name and reasons about string values and control flow, and the transform rewrites identifiers, moves literals into an indexed table and can flatten control flow. Running it afterwards gives you a report that has lost the ability to see real problems, usually with some low-confidence noise about dynamic dispatch attached. Scan the source revision in your repository, then protect the artefact that gets deployed.

Does obfuscation break software composition analysis?

No. Composition analysis works from your package manifest and lockfile rather than from the contents of your bundle, and the transform does not touch those files. Your dependency inventory and your ability to answer whether a newly disclosed vulnerability affects you are unchanged. The separate question of what a consumer of your artefact can determine about its components, and whether a bill of materials survives, has its own write-up.

Why did our vulnerability count drop after we enabled protection?

Most likely because the scanner is seeing less, not because the application became safer. Dynamic scanners discover much of their attack surface by crawling and parsing JavaScript for endpoints and parameters, and that discovery works less well against protected output, so fewer routes are tested and fewer findings are produced. Before treating a shorter report as an improvement, compare the number of routes exercised in each run. A drop there means the coverage changed and the finding count is not comparable.

An auditor scanned our production bundle and sent us hundreds of findings. What do we do?

Explain the input mismatch, then replace their evidence with better evidence. The scanner was pointed at a build output while its rules depend on identifiers and literals that the transform rewrites, which is why the results look the way they do. Supply the static analysis run against the source revision that produced the artefact, then connect the two with the manifest entry recording that source digest and the corresponding output digest. That turns a dispute about tooling into a chain of records they can check.

Can we prove the scanned source and the shipped file are the same build?

That is exactly what a build manifest is for. It records a source digest and an output digest per file, so the artefact in production can be hashed and matched against the entry for the release that your scan results refer to. If you also record the seed, the build is reproducible from the same inputs, which upgrades the claim from a record you are asking someone to trust to a result they can regenerate. Both are ordinary build-time decisions that are difficult to retrofit during a review.

Is it worth turning protection off in a staging environment so scanners work?

It is usually better to run the analysis against source rather than to maintain an environment that differs from production in how it was built. A staging build that skips the transform is no longer the artefact you ship, so results from it carry an assumption you would have to defend anyway. Keep staging as close to the production build as you can, put static analysis in the pipeline before the transform where it belongs, and seed dynamic scans from a route list so their coverage does not depend on parsing the bundle.

Related reading