Shipping code quickly with confidence: Security Testing
Every engineer wants to ship high-quality software systems, but the “how” isn’t always straightforward. To help, we designed a testing series, “Shipping code quickly with confidence.” Using code from sendsecure.ly, a Basis Theory lab project, readers will bring together the testing layers and strategies used by our data tokenization platform to achieve its 0% critical and major defect rate. You can find the links to all published articles at the bottom of each blog.
At Basis Theory, the approach to Security Testing rests on Continuous Testing and Continuous Monitoring. Continuous testing refers to the execution of tests as a part of the SDLC. Continuous monitoring includes the process and tools to check the system(s) and application(s) after deploying and passing the Software Development Life Cycle (SDLC) testing. For this blog, we’ll offer a limited look at the tests we use within our CI/CD pipeline and larger SDLC.
NOTE: Basis Theory secures and tokenizes sensitive data on behalf of its customers. This blog does not detail all the tests, processes, and tools used to protect our PCI Level 1 and SOC 2 certified systems.
Security Testing
Implementations vary too greatly across programming languages and tools at an organization for a one-size-fits-all solution to Security Testing. Instead, we take a holistic approach, checking different components during the SDLC and the system post-deployment of an application according to a pre-defined scope and company policies.
Is my application code secure?
The first place to implement Security Tests is in the codebase itself. Code Scanning and Linting (a.k.a., Static Code Analysis) comprise the first lines of defense. These tools perform checks against the code, looking for known vulnerable implementations in code syntax and ensuring that developers' code does not introduce any known vulnerabilities.
Dependency Checks provide another layer for securing code. These ensure third-party libraries won’t introduce vulnerabilities into the codebase.
Code Scanning
We automate C# code scanning as a part of our build pipelines in GitHub Actions with the NuGet security-scan package.
- name: Security Scan
run: |
dotnet tool install --global security-scan --version 5.6.0
security-scan Foo.Bar.sln --excl-proj="*Tests/**" -n --cwe
Dependency Checks
We use Dependabot, which is a feature we enabled through GitHub. This provides us an easy implementation path to help dependency checking via Dependabot with GitHub Actions. The following YAML file allows this and is a requirement on our repositories.
---
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"
Container Scanning
At the end of the day, the code must run on a system on a server somewhere. Serverless functions are the new hotness and generally employ buildpacks managed for security by the vendor. However, most of these serverless systems now also support running containers. Containers still have a runtime operating system that may contain vulnerabilities and need to be treated as a “Server” for security purposes. Executing container scans as a part of the build pipeline helps ensure the security of the underlying runtime environment.
Automating Container Scanning
To continuously monitor our built containers, we scan them as a part of our build process. Aquasecurity, the developers and maintainers of Trivy, makes this easy with its Trivy GitHub Action
- name: Container Scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: container-name:latest
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
Dynamic Application Scans
Dynamic Application Security Testing (DAST) processes analyze web applications through the front-end or API to find vulnerabilities through simulated outside-in attacks.
At Basis Theory, the exposed APIs comprise the fundamental backbone of our system. Therefore, regular up-to-date scans against the API look for potential vulnerabilities in the endpoints critical to business security. We update our scan configuration based on the Swagger JSON after every deployment to ensure that we constantly scan the most recent API changes. We also perform DAST against the end-user portal as part of this testing. These scans cover the OWASP top 10 vulnerabilities.
Penetration Testing
NIST defines penetration testing as,
“A method of testing where testers target individual binary components or the application as a whole to determine whether intra or intercomponent vulnerabilities can be exploited to compromise the application, its data, or its environment resources.”
Internal/External
The last level of Basis Theory’s internal Security Testing happens at the network level. Existing outside the SDLC, these test the underlying networked systems where the deployed applications live. These penetration tests run against the internal and external networks on a nightly cadence, ensuring that day-to-day changes and operations did not introduce new vulnerabilities, like erroneous open ports, misrouted traffic, and changes in network protocols.
Third-Party
At Basis Theory, we have a third party perform external penetration testing against the system(s) and application(s). These professional red team members, specialized in application security, test rigorously and provide feedback and/or findings that automated tools do not.
Pulse check: Confidence level
The goal of any test is to add confidence that the code, application, or system behaves as expected. So far, we’ve covered Acceptance and Integration Tests, Synthetic Tests, Load Tests, and now Security Tests. So now that we’re through each type, let’s see how confident we are now?
Are we confident that the application can communicate with external dependencies? Yes, API Integration Tests provide confidence that our application can communicate and receive traffic from external dependencies in a deployed environment. E2E Integration tests cover the systems as a whole, replicating user behavior in a production-like environment.
Are we confident that the application’s user interface does what we expect? Yes. The UI Acceptance Tests give us confidence that the UI is behaving as expected.
Are we confident that the application can communicate with external dependencies? Yes, API Integration Tests provide confidence that our application can communicate and receive traffic from external dependencies in a deployed environment. E2E Integration tests cover the systems as a whole, replicating user behavior in a production-like environment.
Are we confident that the application is always available? Yes, we are! Synthetic Tests ensure that our application is healthy and available to serve requests.
Are we confident that the application can handle production throughput and beyond? Yes! Load Tests ensure that our application continues to perform when subjected to production-level traffic patterns.
Are we confident that the application is free of security vulnerabilities? Yes. We have tested all of our deployable layers for any known vulnerabilities. We have confidence in the security of our deployed applications.
Are we confident that the application is secure against common attack vectors (e.g., OWASP)? Yes! We have performed simulated dynamic attacks against our system covering the OWASP Top 10 as well as a broad range of network penetration tests. We have confidence that we are secure against the most common attack vectors.
To learn how we've done this, let's recap the series:
- Testing Layers and Principles
- API Acceptance and Integration Testing
- UI Acceptance and End-to-End Testing
- Synthetic Testing
- Load Testing
- Security and Compliance Testing (this blog)
And that's a wrap. Have questions or feedback? We’d love to hear from you in our Slack community!