Automating dependency vulnerability scanning without over-privileging yourself

Automating a security scan sounds simple until you try to schedule it on a shared cloud account. The scanning logic is the easy part. The hard part is doing it without handing yourself, or the job, more power than the task actually needs. This is a walk through building a recurring dependency vulnerability sweep, and the least privilege discipline that shaped every decision along the way.

The goal

The brief was a weekly dependency vulnerability sweep across a selected set of projects. For each one, run the ecosystem’s audit tool against live advisory databases, the standard package audit for the JavaScript projects and the equivalent for the Python ones, then send a digest email with a readable PDF report attached. Nothing exotic. The interesting engineering was in the constraints, not the scanning.

Opt in, never auto discover

The first design decision was to make scanning opt in. The tempting alternative is to crawl the disk, find every dependency manifest, and scan whatever you discover. Do not do this. On any real machine you will drag in vendored dependency trees, third party code, and example projects that you have no business scanning and no authority to raise findings against. The noise buries the signal and you end up reporting vulnerabilities in code that is not yours to fix.

A project is scanned only if it appears on an explicit manifest that a human maintains. If it is not on the list, it does not get touched. This keeps the report about the things you actually own and are accountable for, and it means adding a project is a deliberate act rather than an accident of where a file happened to live.

The least privilege wall

Here is the part that taught the most. To wire up the recurring trigger, a scheduler that kicks off a build job on a timer, the setup needed a specific permission to hand a role to the build service. That permission is exactly the kind of thing you should not be holding by default, because the ability to pass roles around is the ability to escalate privilege. We did not have it, and that was correct.

The wrong move at that point is to request a broad grant to make the error go away. The right move is to prove precisely what is missing and ask for exactly that and nothing more. So rather than assume, we tested the gap empirically: simulate the action, make the real calls, watch exactly which permission is denied and on which resource. With that evidence in hand, the request was for one scoped grant that closed one specific gap. No wildcards, no “give me the broader policy to be safe.”

This is slower than asking for broad access, and it is worth every minute. A request that says “I need this one permission, on this one resource, and here is the failing call that proves it” is one a reviewer can actually reason about and approve with confidence. “Give me the admin policy so I stop hitting walls” is not.

How the architecture evolved

The first version was an always on worker that ran its own internal timing loop, waking itself up on a schedule to do the sweep. This works, but it means paying for a container that sits idle almost all of the time, existing only so that it can check its watch.

Once the scheduling permission landed, the design changed. The recurring trigger became an external scheduler that starts a build only job on the timer. The build runs, does the sweep, sends the report, and exits. Nothing sits idle between runs. It is cheaper and simpler, because the thing that knows about time is now a managed scheduler rather than a loop you wrote and have to keep alive.

One detail worth copying: rather than delete the old always on worker outright, we left it scaled to zero. It costs nothing at zero, and it is an instant rollback. If the scheduled job had gone wrong, flipping the old worker back up was a one step return to a known good state.

The two identity trap

The move from one run mode to another surfaced a classic bug. The always on worker and the scheduled build job run under different identities. Cloning the private projects requires reading a secret, the access token, and the grant to read that secret had been attached to the worker’s identity. The build job’s identity did not have it. So the scan worked perfectly in the old path and failed the moment it ran in the new one, for a reason that had nothing to do with the scan itself.

The lesson is to treat every permission the work depends on as something the new identity needs too, not something the code carries with it. When you change how a job runs, walk through everything it reads, writes, and calls, and confirm the identity behind the new run mode can do each of those. The permissions do not follow the code across the cutover. You have to bring them.

The takeaway

The scanning was the least interesting part of building a scanner. The value was in the discipline around it: scan only what you explicitly opted in, prove the exact permission you are missing instead of asking for broad access, let a managed scheduler own the timing, keep the previous design scaled to zero as a rollback, and remember that permissions do not travel with your code when the identity changes. Get those right and the security tool does not itself become the thing that needs securing.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You can manage the subscriptions of this post.