Security Testing
Security testing verifies that your application resists attack. It encompasses static analysis of source code, dynamic testing of running applications, fuzzing for unexpected inputs, and continuous scanning of dependencies. The goal is to find vulnerabilities before attackers do.
Static Application Security Testing (SAST)
SAST analyzes source code for vulnerabilities without running the application. It catches issues early, integrates into CI, and runs fast. The trade-off is false positives and inability to detect runtime issues.
cargo audit
Checks your Cargo.lock against the RustSec advisory database. This is the Rust equivalent of npm audit or Dependabot.
# Install
cargo install cargo-audit
# Check for known vulnerabilities
cargo audit
# Output example:
# Crate: chrono
# Version: 0.4.19
# Warning: unsound
# ID: RUSTSEC-2020-0159
# URL: https://rustsec.org/advisories/RUSTSEC-2020-0159
cargo audit should run on every pull request. A known vulnerability in a dependency is the easiest attack vector to exploit and the easiest to prevent. The Equifax breach (2017, 147 million records, $1.4 billion cost) happened because a known vulnerability in Apache Struts went unpatched for months. cargo audit in CI prevents this class of failure.
cargo deny
Policy enforcement for dependencies. Goes beyond cargo audit to cover licenses, sources, and duplicate crates. Configuration lives in deny.toml.
# deny.toml
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause"]
confidence-threshold = 0.8
[bans]
multiple-versions = "warn"
deny = [
# Deny specific crates known to be problematic
{ name = "openssl" }, # Prefer rustls
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
cargo install cargo-deny
# Check all policies
cargo deny check
# Check specific categories
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources
cargo clippy
Not strictly a security tool, but catches code patterns that can lead to security issues: integer overflow, unused Result values (ignoring errors), and unreachable patterns.
# Run with all warnings as errors
cargo clippy -- -D warnings
# Enable additional security-relevant lints
cargo clippy -- -W clippy::unwrap_used -W clippy::expect_used
Disallowing unwrap() and expect() in production code prevents panics that can cause denial of service or leak information in error messages.
Dynamic Application Security Testing (DAST)
DAST tests the running application by sending requests and analyzing responses. It finds issues that static analysis cannot: misconfigurations, missing headers, improper error handling at runtime, and business logic flaws.
Tools:
- OWASP ZAP -- Free, open-source. Automated scanning, manual testing, and API fuzzing. Can be run headless in CI.
- Burp Suite -- Industry standard for professional penetration testing. Community edition is free; Pro edition adds automated scanning.
- Nuclei -- Template-based vulnerability scanner. Fast, extensible, and suitable for CI integration.
Running OWASP ZAP in CI:
# GitHub Actions example
security-scan:
runs-on: ubuntu-latest
services:
app:
image: your-app:latest
ports:
- 8080:8080
steps:
- name: OWASP ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.9.0
with:
target: "http://app:8080"
rules_file_name: "zap-rules.tsv"
fail_action: "true"
DAST complements SAST. SAST finds code-level flaws; DAST finds deployment and configuration flaws. Use both.
Fuzzing for Security
Fuzzing feeds random or mutated inputs to your code to discover crashes, panics, and unexpected behavior. In Rust, a panic in a server handler is a denial-of-service vulnerability. Fuzzing is especially valuable for parsers, deserializers, and any code that processes untrusted input.
cargo-fuzz (libFuzzer)
cargo install cargo-fuzz
# Initialize fuzzing targets
cargo fuzz init
# Create a fuzz target
cargo fuzz add parse_input
// fuzz/fuzz_targets/parse_input
// Fuzz target definition
FUZZ_TARGET(data : bytes):
IF data IS VALID UTF-8 THEN
s ← CONVERT_TO_STRING(data)
// Fuzz the parser -- it should never panic
CALL PARSE_INPUT(s) // ignore result, just check it does not crash
END IF
# Run the fuzzer (runs indefinitely until stopped or crash found)
cargo fuzz run parse_input
# Run with a time limit for CI
cargo fuzz run parse_input -- -max_total_time=300
Property-based testing with proptest
Not traditional fuzzing, but serves a similar purpose: automatically generating inputs to find edge cases.
// Property-based tests with automatically generated inputs
TEST parse_never_panics:
FOR EACH input IN RANDOM_PRINTABLE_STRINGS DO
// Property: PARSE_INPUT should never panic, regardless of input
CALL PARSE_INPUT(input) // ignore result, just verify no crash
END FOR
TEST validated_email_roundtrips:
FOR EACH email IN RANDOM_STRINGS MATCHING "[a-z]{1,10}@[a-z]{1,10}.[a-z]{2,4}" DO
parsed ← EmailAddress.PARSE(email)
ASSERT parsed.AS_STRING() = email
END FOR
Dependency Scanning
Dependencies are the largest attack surface for most applications. Most Rust projects pull in hundreds of transitive dependencies, each of which can introduce vulnerabilities.
Automated dependency updates
Dependabot (GitHub) or Renovate (self-hosted) automatically open pull requests when new versions of your dependencies are available, including security patches.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Software Bill of Materials (SBOM)
An SBOM is a complete inventory of all components in your software, including transitive dependencies. After Log4Shell (2021), SBOMs became a critical part of supply chain security. Most affected organizations did not even know they depended on Log4j.
# Generate SBOM in CycloneDX format
cargo install cargo-cyclonedx
cargo cyclonedx --format json
Secret scanning
Prevent credentials from being committed to version control.
# Install gitleaks
brew install gitleaks
# Scan repository history
gitleaks detect --source . --verbose
# Scan staged changes (use as pre-commit hook)
gitleaks protect --staged
GitHub's built-in secret scanning detects API keys, passwords, and tokens automatically for public repositories and with GitHub Advanced Security for private repositories.
CI Integration
All security testing should run automatically. Manual security reviews do not scale and will be skipped under deadline pressure.
Complete CI security pipeline
# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Audit dependencies
run: cargo audit
deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cargo-deny
run: cargo install cargo-deny
- name: Check advisories
run: cargo deny check advisories
- name: Check licenses
run: cargo deny check licenses
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Clippy with security lints
run: cargo clippy -- -D warnings -W clippy::unwrap_used
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
fuzz:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Fuzz parsers (5 min)
run: cargo fuzz run parse_input -- -max_total_time=300
What to run when
| Check | On every PR | On merge to main | Weekly |
|-------|-------------|-------------------|--------|
| cargo audit | Yes | Yes | Yes |
| cargo deny check | Yes | Yes | Yes |
| cargo clippy | Yes | Yes | -- |
| Secret scanning | Yes | Yes | Yes (full history) |
| Fuzzing (short) | Optional | Yes | -- |
| Fuzzing (long) | -- | -- | Yes |
| DAST scan | -- | Yes (staging) | Yes |
| Full penetration test | -- | -- | Quarterly |
Trade-offs
| Approach | Pros | Cons | |----------|------|------| | SAST | Catches bugs early, fast, integrates into CI | False positives, cannot detect runtime issues | | DAST | Finds runtime misconfigurations, tests real behavior | Slower, requires running application, may miss code-level flaws | | Fuzzing | Finds edge cases humans miss, especially in parsers | Requires setup, can be slow, non-deterministic | | Dependency scanning | Catches known vulnerabilities automatically | Only finds known vulnerabilities, not zero-days | | Secret scanning | Prevents credential leaks | False positives on test data, cannot find secrets already rotated |
Key Takeaways
- Run
cargo auditandcargo denyon every pull request. This is non-negotiable for any project with dependencies. - SAST and DAST are complementary, not alternatives. Use both.
- Fuzz all code that processes untrusted input: parsers, deserializers, validators.
- Automate everything. Security checks that require manual action will be skipped under pressure.
- Generate and maintain an SBOM. When the next Log4Shell happens, you need to know within minutes whether you are affected.
- Prevent secrets from entering version control. Use pre-commit hooks with gitleaks and enable GitHub's built-in secret scanning.