Bullet Proof Software engineers security into software from the first design session – not as an audit afterward. We are the SoftwareMile specialist practice for secure software engineering.
Secure Development Lifecycle
Threat modeling at design time, secure coding standards during build, dependency and supply-chain checks in CI, security testing before release, and hardening at deployment. Security activities are part of the definition of done, not a separate phase.
Application Security Reviews
Secure code review of the paths that matter – authentication, authorization, data handling, file processing, payment flows – with findings ranked by exploitability and fixes proposed in code, not just in a report.
API and Cloud Security Engineering
API authentication and rate design, least-privilege cloud IAM, secrets management, network segmentation, and logging that supports investigation. We build on AWS, Azure, and GCP with the platform security services each provides.
Resilience Engineering
High-availability architectures, backup and disaster-recovery design with tested restore procedures, and business-continuity planning proportionate to what downtime actually costs you.
What We Do Not Claim
We are engineers, not auditors: we do not issue compliance certifications, and we say so plainly. We build and review software so it stands up to scrutiny – yours, your customers, and your assessors.
Describe your application and hosting environment – include your architecture, authentication approach, and the security concern that prompted the search. Assessment methods and reporting samples are shared during scoping.
Related services
- Penetration Testing and Security Assessment — Verifying the result of secure development: penetration testing and security assessment.
- Security Compliance Readiness — Preparing for an audit or customer security review: security compliance readiness.
Threat Modeling Without the Ceremony
Threat modeling has a reputation for heavy diagrams and documents nobody opens twice. The version worth keeping is short. At design time, sketch the trust boundaries, list what an attacker gains by crossing each one, and write down the decisions and the tests that follow from that. Keep it in the repository beside the code so it ages with the system. Reserve deeper treatment for the parts that warrant it: authentication, payments, tenant separation, file handling, anything touching regulated data. A model that gets updated when the design changes is worth more than an exhaustive one produced once and archived.
Proportion applies to the whole program, not only to the model. An internal tool with no personal data, few users and a small blast radius does not need a formal process wrapped around it, and coding standards, dependency hygiene, code review and sensible framework defaults cover a great deal. The threshold worth watching is the point where the system starts holding data you would have to notify someone about, becomes reachable from the internet, or begins storing credentials to other systems.
The Bugs Tools Are Bad At
Authorization bugs are specification bugs, and no scanner knows what your rules are supposed to be. That is why the technique comparison on our security technologies page puts this whole category on the manual side of the line. Reviewing those paths means reading the code with the intended rules in hand and asking concrete questions.
- Can user A load user B’s invoice by changing an identifier in the request?
- Can one tenant reach another tenant’s storage prefix or search index?
- Can a workflow skip an approval step by calling the later endpoint directly?
- Can a payment or provisioning step be replayed?
- Does an expired or downgraded account still hold access it should have lost?
Reviewing a Codebase You Did Not Write
Inherited systems are common. The original team has moved on, documentation is thin, and the deployment has drifted from whatever was designed. A workable order of attack is authentication and session handling first, then authorization on every data access path, then anything accepting a file or a URL, then background jobs and scheduled tasks where authorization is frequently assumed rather than checked. Coverage is bounded by the time available and the access we are given, and a review of this kind produces a prioritized picture with the areas we did not reach stated plainly. No review of a substantial codebase should be described as complete.
Security Requirements Belong in Tickets
Security work living in a separate document competes with delivery and loses. Written as acceptance criteria, it gets estimated, built and tested like everything else in the sprint. Useful criteria are testable statements: this endpoint rejects a request for a record the caller does not own; uploaded files are stored outside the web root and served through a handler that checks permissions; this token expires and refresh requires reauthentication. The test proving it should fail if somebody removes the control, which is what makes it a regression test and not a paragraph.
Resilience Has Trade-offs Too
High availability adds components, and every component is a new failure mode and another thing to patch. Multi-region designs introduce consistency questions that are hard to reason about calmly under pressure. The proportionate answer depends on what an outage means for your particular business, and that varies enormously. A nightly reporting job and a payment gateway do not deserve the same architecture or the same budget of complexity. We design backup and recovery so the restore procedure is something your team can rehearse, and we run the first rehearsal with you; whether it keeps happening after that is yours to own. The reasoning behind that testing is expanded on our ransomware recovery and disaster recovery validation page, which describes a product still in development.
We Review. We Do Not Certify.
A review produces evidence for your decision. It is not an assurance opinion, and we issue no certificates, attestations or sign-off letters of any kind. Where we have written or remediated the code, we are also the wrong party to assure it, for the same reason a builder does not inspect their own work. Our security compliance readiness page states the same separation for SOC 2 and ISO 27001, where the examination belongs to a licensed CPA firm and the certification audit to an accredited body. Where a customer or a regulator needs an independent opinion, it has to come from someone with no stake in the work being reviewed, and we would rather say so early than blur the line.
Triaging a Dependency Advisory
The supply-chain check in CI produces a list, and the list needs triage before it needs work. The first question is what actually shipped: the manifest declares a version range, the lockfile records the resolved version, and a scanner pointed at the manifest can report a different answer than the one running in production. The second question is reachability. A vulnerability in a parser your application never calls, reached only through an option that is off by default, is not the same finding as one in the code path that handles uploaded files, even when the two carry the same score. A severity rating is calculated against an assumed deployment, and yours may not be it.
The upgrade path is usually the real work. When the patched release exists only for a major version you are not on, the fix stops being a version bump and becomes a migration, and the interim choices are a pinned build, a configuration change that closes the exposed path, or a control at the edge that blocks the input shape. Each of those is defensible with a date attached to it and indefensible without one, because the temporary measure is what survives when nobody revisits the ticket.
- Is the package a direct dependency, or transitive, and can it be resolved by upgrading the parent rather than forcing an override that the parent was not tested against?
- Does the application call the affected function at all, and is the input reaching it attacker controlled?
- Does the advisory assume a configuration you actually run: a particular parser, a server mode, a feature flag, an option that ships disabled?
- Is it in the known exploited catalog, which is the strongest argument for treating it as urgent whatever your reachability analysis says?
- Does a fixed release exist for the major version you are on, or does the fix require a framework migration and its own test plan?
Secrets, and What Rotation Means
Deleting a secret from a file does not end the exposure. The value is still in the git history, and it is also wherever it already traveled: build logs, container image layers, a developer's shell history, a clone somebody made last year, a backup of the repository. The only action that ends it is rotating the credential at whatever issued it. Rewriting history is cleanup, and it belongs after rotation rather than instead of it. A rotated secret is also only an improvement if the replacement is not stored the same way the original was.
The direction that removes the problem rather than managing it is short-lived credentials. A CI job can federate to a cloud account through OIDC and receive a token scoped to that run, so the pipeline holds no long-lived access key at all, and the trust lives in a policy in the cloud account that names the repository and the branch. That policy is worth reading character by character, because a subject condition written to match the organization but not the repository lets any repository in the organization assume the role. Databases and internal services can reach the same place with short-lived certificates or a broker that issues per-session credentials, which turns a leak into a problem with an expiry time.
Gates in the Pipeline
A gate that blocks a merge has to be trustworthy or it gets switched off, and the team learns that security checks are noise. The way to arrive at a blocking gate is to start advisory, baseline the findings that already exist so the gate applies to new code, and promote rules to blocking one at a time as their false positive rate becomes known. A gate on what the diff introduces is enforceable in a way that a gate on the whole repository's backlog is not, because the developer holding the pull request can actually act on it.
Secrets detection is the exception that belongs as early as possible, ideally before a push reaches a shared remote, since once a value lands on a remote branch rotation is required regardless of what happens next. The pipeline configuration deserves review of its own. A workflow that runs in the base repository's context so it can read secrets, and then checks out and executes the code from a pull request branch, hands those secrets to anyone who opens a pull request, and this is a well known way to give them away. Log masking helps but is best effort: it matches the literal value, so a secret that has been base64 encoded, concatenated, or split across variables prints in the clear.
Placement matters as much as choice of tool. Checks measured in seconds belong on every push, checks measured in tens of minutes belong on a schedule or a release branch, and anything that needs a deployed environment belongs after deploy to staging rather than in front of the merge. A check in the wrong place is not a stricter program, it is a slower one that people learn to route around.
Defaults That Get Defeated
Frameworks ship safe defaults, and a large share of what a review finds is a place where somebody turned one off deliberately, for a reason that was reasonable that afternoon. The query builder parameterizes everything except the one raw string that was needed for a dynamic sort column. The template engine escapes output everywhere except the filter that marks a value safe, added so a rich text field would render. The CSRF middleware is exempted on one endpoint so a partner could post to it, and the exemption outlives the partner by years.
This is what makes an inventory of exemptions a productive early step: the escape hatches have names, they are searchable, and each one comes with a question about who defeated the default and why. The finding worth writing up is rarely that the escape hatch exists. It is that nobody left a reason, and the condition that justified it is no longer true.
- A template filter marking a string as safe, wrapping a value that originated with a user.
- A CSRF exemption on an endpoint that still authenticates with a session cookie, which is what the protection existed for.
- An HTTP client built with certificate verification disabled, usually left behind from a staging environment with a self-signed certificate.
- CORS configured to reflect the request Origin header with credentials allowed, which is equivalent to accepting every origin.
- A concatenated raw query sitting next to an ORM used correctly in the rest of the file.
- A route missing the authorization decorator that every sibling route in the controller carries.
Which Engagement Fits
The choice between a review and a test is usually settled by what you already know. If the question is whether authorization is enforced on paths a tester would have to guess the existence of, reading the code reaches them and a black-box test may not. If you already hold a list of suspected problems, fixing them and testing afterward gets more out of the test, because a test aimed at known unfixed issues spends its budget confirming a list you already had.
Compliance readiness is a third question and it is worth keeping separate. An assessor asking for evidence of a secure development process is asking about the process: who approves changes, what the pipeline enforces, how findings are tracked to closure. That is different work from establishing what the code actually does on a given path. Teams often arrive asking for one when the date on the calendar requires the other, and the fastest way to tell them apart is to look at who is waiting for the answer.
There are cases where the honest answer is a different kind of work entirely. If the application is a thin layer over managed services and most of what you own is configuration, then reading IAM policies, storage permissions, network rules and identity provider settings will find more than reading application code will. And if there has never been a conversation about trust boundaries, that conversation should come before either engagement, because it changes what the scope ought to include.
Frequently Asked Questions
What does an application security review actually consist of?
It is reading, not scanning. A reviewer builds an inventory of entry points, which means routes, queue consumers, scheduled jobs and anything else that accepts input, then reads the paths in scope against the authorization rules the system is supposed to enforce. Findings are written with the file and the condition that makes them exploitable, and with a proposed change in code rather than a category label.
What do you need from us before a review can start?
Read access to the source at a specific commit or tag, a description of the architecture and the authentication approach, and a named person who can answer questions about intent. If the system spans several repositories, we need to know which are in scope, since a monolith and its three satellite services are four decisions about coverage, not one. A running staging environment is useful for confirming behavior but the source is the requirement.
Who has to define what the authorization rules are supposed to be?
Someone on your side, by name, who can state what each role is allowed to do and which records each user is allowed to reach. If nobody can state them, that is itself a finding, and writing them down becomes the first deliverable rather than a prerequisite. In most systems the rules are distributed across product decisions, support practice and the code, and reconciling those three is often where the interesting disagreements surface.
Do you need production access or administrative rights in our cloud account?
No for code review, and read-only for cloud configuration work. That is a role such as ReadOnlyAccess or SecurityAudit on AWS, Reader or Security Reader on Azure, or viewer plus security reviewer on GCP, scoped to the accounts in question. Configuration changes come back as infrastructure-as-code diffs or written steps that your team applies, so the change and the approval stay inside your process.
What if our deployment has drifted from what is in the repository?
Establish which one is the subject before anything else, because reviewing code that is not what runs is wasted effort. Comparing the deployed container image digest against what the pipeline built, checking framework and library versions reported by the running application against the lockfile, and looking at build metadata will usually settle it quickly. When they diverge, the drift is worth understanding on its own, since it means changes are reaching production through a path the pipeline does not control.
Does our language or framework change how the review is done?
It changes where to look, not what to look for. The common failure is a defeated default rather than a missing one, so each stack has its own short list of escape hatches worth inventorying first. Memory-unsafe languages add a class of bug that managed runtimes largely remove, but the authorization and business-logic questions are identical across all of them and those are usually the ones that matter.
What do we have to decide before high-availability or recovery work?
Two numbers and an owner. How much data you can afford to lose and how long you can be unavailable are what select an architecture; picking the architecture first and deriving the numbers from it gets the order backwards. The other decision is who performs a restore and how often they practice, and the restore has to cover everything outside the database as well: object storage, secrets, DNS, certificates and the deployment configuration itself.