Bullet Proof Software builds security into your delivery pipeline so every release is checked the same way, every time – without slowing your team to a crawl.
Pipeline Security That Developers Accept
SAST, SCA, and secret scanning wired into CI with tuned rulesets – because a pipeline that cries wolf gets ignored. Findings route to the developer who introduced them, with context, while the build is still fresh.
Software Supply-Chain Security
Dependency pinning and review, build provenance, artifact signing, and an inventory of what actually ships. When the next major dependency vulnerability lands, you will know in minutes whether you are exposed – not after a weekend of grepping.
Infrastructure as Code and Cloud Posture
IaC scanning, drift detection, least-privilege service identities, and guardrails that prevent misconfigurations from reaching production rather than reporting them afterward.
Practical Rollout
We start with your current pipeline and add controls in an order that pays off immediately: secrets first, then dependencies, then static analysis, then policy gates. Each step lands with the tuning and documentation your team needs to own it.
Tell us about your pipeline – CI platform, deployment targets, and languages. We will propose a rollout sequence with effort estimates per step.
Related services
- Penetration Testing and Security Assessment — Pipeline scanning is not a substitute for testing – see penetration testing and security assessment.
- Security Compliance Readiness — Turning pipeline evidence into audit evidence: security compliance readiness.
What Happens to the Findings You Already Have?
Turning a scanner on across an existing codebase produces a backlog nobody asked for, and the usual result is that the team stops looking at any of it. The workable approach is to baseline what already exists, block on newly introduced issues, and burn the historical set down deliberately as code gets touched for other reasons. A gate that fails on something you wrote an hour ago is arguable. One that fails on code nobody in the room wrote is just an obstacle, and it teaches people to route around the whole program.
Which Gates Should Actually Block a Build
Blocking is a budget, and it gets spent carelessly. Secrets in a commit and known-malicious or typosquatted packages are worth stopping outright, because there is no argument about what they are or what to do about them. Broad static analysis rulesets usually are not, at least at first, because false positives on a mature codebase can burn the credibility of the entire program before it is established. A defensible progression runs everything in reporting mode, tunes until the remaining findings are ones a developer agrees with, and only then promotes a rule to blocking. Gates have to earn their authority.
Scale the program to your release rate and to the number of people who can realistically own it. A small team shipping on a manual cadence does not need policy engines or admission controllers, and more tooling than the team can tune produces dashboards nobody opens.
A Leaked Secret Is Not Fixed by Deleting the Commit
Removing a credential from repository history does nothing whatsoever about the credential. Rotate it first, then check whether it was used and from where, then clean history if you still want to. History rewriting on a shared repository carries its own coordination cost, and it does not reach forks, existing clones, CI caches, or whoever scraped the repository while it was briefly public. The most useful outcome of secret scanning is a rotation habit that nobody has to be argued into.
Metrics That Mislead
Counting findings measures how noisy your tools are. Counting scanners deployed measures procurement activity. Better signals are how long a confirmed, reachable, high-severity issue stays open, whether new repositories inherit the pipeline automatically or because somebody remembered, and how often a gate gets bypassed along with the reason given. Bypass frequency deserves reading as a design signal before it is treated as a discipline problem. Where people route around a control regularly, something is wrong with the control, with the process it interrupts, or with both, and the reasons people type into the bypass field are where to look first.
Supply Chain Beyond Scanning
Dependency scanning reports known vulnerabilities in what you declared. It says very little about build integrity. Pinned dependencies, recorded build inputs, and short-lived pipeline credentials issued through federated identity matter as much as the scan output does. Where that thinking leads is described on our supply chain control plane page, which is a product concept in development and not something available to buy today.
Where a Check Runs Decides What It Can Find
The same scanner behaves differently depending on where it runs. There are four placements, and they are not substitutes for each other: on the developer's machine before a commit, on the pull request, on the default branch after a merge, and on a schedule against what is currently deployed. Most programs install one of them and treat the rest as redundant.
Pull request checks stay diff-scoped and return quickly, because their cost is paid on every push by every developer. Diff scoping is also what limits them. A SAST rule that follows data from an untrusted input to a dangerous sink cannot see the path when the source and the sink sit in files the diff never touched, and no check running at merge time can flag a dependency whose vulnerability was disclosed a month after the code landed. That is what a scheduled full run against the default branch is for, and it is the placement most often skipped.
SCA does not diff-scope in any meaningful way. Resolving a manifest and its lockfile produces the entire transitive graph whether one dependency moved or fifty, so the run costs roughly the same either way. Container image scanning has to happen after the build rather than against the source tree, because the base image contributes packages nothing in the repository declares, and the version in the image is the version an attacker gets.
In a monorepo, path filters are what stop every pull request from running every check, and they interact badly with required checks. On GitHub Actions a workflow skipped by a paths filter leaves its check pending rather than successful, so a pull request that requires that check never becomes mergeable. The usual handling is a second job carrying the same check name that always runs and reports success when the paths did not match.
The Pipeline Is Also an Attack Surface
The pipeline is not only where security checks run. It holds deployment credentials, executes third-party code on every build, and frequently has write access to the repository it builds. It tends to be governed like a development tool rather than like production, because a development tool is what it was when somebody set it up.
Reviewing it means reading workflow definitions and runner configuration rather than scanning source. These are the patterns that recur:
- Workflows that expose repository secrets to code from forks. On GitHub Actions the distinction is pull_request, which gives a fork's build a read-only token and no access to secrets, versus pull_request_target, which runs the base repository's workflow with a write-scoped token and secrets available. The dangerous shape is a pull_request_target job that checks out the pull request head and then builds it.
- Third-party actions, plugins, and shared pipeline templates referenced by a branch or a tag instead of a commit SHA. Tags are mutable, so the reference resolves at run time and the code that executes is not necessarily the code anyone reviewed.
- Self-hosted runners that persist state between jobs. Workspace files, container layers, and cached credentials from one job stay readable by the next, and a runner registered at the organization level can accept jobs from repositories with very different trust levels.
- Build caches shared across branches. A run can restore entries its base branch created, so a job writing the cache from a less trusted context is supplying input to later builds in a more trusted one, and the cached bytes are rarely verified against anything.
- Default job tokens carrying write scope that no job in the workflow uses. GitHub Actions takes the default from an organization or repository setting and lets a workflow narrow it with a permissions block; GitLab constrains what CI_JOB_TOKEN can reach through a per-project allowlist. Narrowing costs a line and breaks nothing that was not already depending on the extra scope.
- Artifacts and logs that carry more than the build output. Uploading a whole workspace captures whatever the job wrote into it, including rendered config, kubeconfigs, and credential files, and artifacts are downloadable by everyone with read access to the repository.
Secret Detection Fails in Predictable Ways
There are two detection strategies and they fail in opposite directions. Pattern matching keys on known credential formats: the ghp_ and github_pat_ prefixes, AWS access key IDs beginning AKIA or ASIA, Stripe keys beginning sk_live_. It is precise and blind to anything unprefixed, which covers database connection strings, private keys pasted into config, and the signing key of any internal service. Entropy scoring catches the shapeless ones and pays for it in noise, because UUIDs, test fixtures, lockfile integrity hashes, base64 images, and minified bundles all score high.
Validation is what separates a candidate string from an incident. A scanner that calls the issuing service to see whether a credential still authenticates turns a long list of suspicious strings into a short list of live ones, and only the second list is worth interrupting somebody for. It works only for services that publish a cheap identity endpoint, it makes outbound network calls from wherever the scan runs, and providers rate limit it, so validation is usually applied to what the pattern matchers found rather than to everything entropy flagged.
Log masking is not a control to lean on. CI systems mask by registering a value and replacing literal occurrences of it in output, so a secret that is base64 encoded, URL encoded, JSON escaped, or split across lines prints in the clear, because the bytes on the line are no longer the bytes that were registered. Masking also covers the log stream and nothing else: a value written to a file, uploaded in an artifact, or posted to an external service is outside it entirely.
The one placement that prevents rather than detects is push protection at the forge, where the check runs before the object is accepted and the credential never enters shared history. Everything else is detection with a shorter delay, pre-commit hooks included: hooks live in the local clone, are not distributed by cloning unless a hooks path is committed and configured, and are skipped by git commit --no-verify.
What This Depends On From Your Side
The first dependency is administrative rather than technical. Making a check required means changing branch protection or merge request approval settings, which usually lives at the organization level; installing a scanner as an app needs someone who can install apps; and adding the job needs write access to workflow files in repositories that may belong to teams other than the one asking for the work. Where that authority is split, sequencing gets decided by who can approve what, not by which control is worth the most.
The secrets step depends on a rotation path existing before the scan runs. The constraint is never detection. It is knowing who can rotate a given credential, what stops working while it rotates, and whether anything outside the repository holds a copy: a scheduled job, a partner integration, a device image, a runbook. A credential that cannot be rotated without a change window is a finding you cannot close the day you find it, and it is worth knowing which ones those are in advance.
The dependency step depends on resolution being reproducible. A project that installs from a floating version range at build time gets a different package set per build, so the scan describes one build rather than the artifact you shipped. Committing a lockfile is frequently the larger piece of work. The same holds below the language layer, where a Dockerfile running an unpinned package install produces a different image every time, and the scan result expires with it.
Routing depends on an accurate map from repository to team. CODEOWNERS entries drift, groups get renamed, and a finding assigned to a team that no longer exists is a finding nobody sees. Checking that map is worth doing before the first scan rather than after, because a routing failure and a tool nobody trusts produce the same symptom: findings sitting untouched.
Ownership, and When Less Is the Right Answer
A gate needs a named owner before it is turned on, and the owner is whoever can change the rule, not whoever can approve the merge. Those are usually different people. The distinction decides what happens the first time a check fails for a reason unrelated to the change in front of it: a ruleset update that altered behavior, an advisory that was later withdrawn, a registry rate limiting the dependency step, a scanner version that started flagging a pattern it used to ignore.
The practical test for a new gate is not whether the rule is correct. It is whether a developer looking at a red check can tell in about a minute whether the finding concerns their change, and knows who to ask when it does not. A gate that fails that test turns every ambiguous finding into an interruption for someone who cannot resolve it either.
Not every team needs an engagement for this. With a handful of repositories on one platform and one team owning all of them, most of the first pass is configuration you can do yourself: turn on the forge's native secret scanning with push protection, enable its dependency alerts, pin third-party actions to commit SHAs, set the default job token to read-only, and commit lockfiles where they are missing. Configuration stops being enough at the point where the same checks have to apply identically across repositories owned by different teams, and where somebody has to keep them tuned as the rulesets change underneath them.
Frequently Asked Questions
What does this actually change in our repositories?
Files and settings, mostly. Pipeline or workflow definitions that add jobs, a configuration file per scanner holding the enabled ruleset and its pinned version, a baseline recording what already existed, branch protection or approval rules marking specific checks as required, and the routing configuration that decides who receives a finding. Whether a given scanner runs entirely on your runners or calls a hosted service is a per-tool choice worth making deliberately, because it determines where your source and your dependency inventory end up stored.
Which CI platforms does this apply to?
The shape is the same on GitHub Actions, GitLab CI, Azure Pipelines, Bitbucket Pipelines, CircleCI, and Jenkins: a job definition, a place to hold credentials, and a merge check the platform can require. What differs is enforcement. GitHub uses required status checks under branch protection or rulesets, GitLab uses merge request approval rules plus pipeline configuration it can enforce from the group level, and Jenkins has no native merge gating at all, so a Jenkins-based program still relies on the forge in front of it to block the merge.
What has to already exist before any of this is worth doing?
A pipeline that builds and tests the code on a server rather than on a laptop, somewhere to store pipeline secrets that is not the repository, and a lockfile or equivalent for each project. You also need administrative access to whatever setting makes a check required, since that is what turns a job into a gate. If builds are not reproducible on the CI system yet, that is the first project, because a scan of an unreproducible build describes something you cannot build twice.
How does a new repository end up covered?
Coverage is either a property of how repositories get created or it is manual. A template repository carries the workflow files into everything created from it. Above that, GitHub enforces workflows across repositories through organization rulesets and GitLab through compliance pipeline configuration, both of which apply a job to repositories matching a rule regardless of what those repositories have in their own config. A one-time script that pushes a workflow file into every repository covers the repositories that exist on the day it runs.
What do we have to decide that cannot be decided for us?
A few things that are policy rather than engineering. What severity, in what context, is worth interrupting a release for. Whether an unmaintained dependency with no known vulnerability is a security problem or a maintenance one. Who may approve an exception. And whether license findings belong in the same gate as vulnerability findings, since SCA tools commonly ship both in one ruleset and license policy is a legal question that engineering should not be answering by configuration default.
Do developers need to run anything on their own machines?
No, and it should not be required. Pre-commit hooks reduce round trips rather than enforce anything, since they live in the local clone and can be skipped at commit time. What developers do need is a way to reproduce a finding locally with the same scanner and the same ruleset version in one command. Without that, every disputed finding becomes a pipeline rerun and an argument about the tool instead of about the code.
How do exceptions and bypasses get handled?
A gate with no exception path gets removed. Something will eventually block a release for a reason that is real and not the release's fault: a vulnerability with no fixed version published, a rule that is right most of the time and wrong here, a dependency you cannot move until a downstream service moves first. The alternative to a documented exception is an administrator switching the check off for everyone, which is a much larger change made much more quietly. Exceptions should carry an expiry and a recheck date, because suppression files otherwise only grow, and an entry added for a two-week workaround outlives everyone who knew why it was added.