Penetration Testing Automation: Tools and Workflows for Continuous Security
Penetration testing automation runs security checks continuously and repeatably rather than as annual point-in-time assessments. Tools like Nuclei (vulnerability templates), Nmap (port scanning), SQLMap (SQL injection), and Nikto (web server scanning) can be combined into a pipeline that runs on every deployment. This guide covers how to build that pipeline for authorized environments.
What Penetration Testing Automation Is (and Isn't)
Automated penetration testing is not a replacement for manual penetration testing by skilled security professionals. It's a tool for:
- Regression testing — verify that known vulnerability patterns aren't reintroduced after fixes
- Continuous monitoring — catch obvious security misconfigurations as soon as they're introduced
- Pre-pentest reconnaissance — identify easy targets before manual testing focuses on complex logic issues
Important: Automated tools have high false positive rates and miss complex vulnerabilities (business logic flaws, authentication bypasses, chained exploits). They find known patterns; they don't find novel vulnerabilities.
Authorization requirement: Never run these tools against systems you don't own or have explicit written authorization to test.
Tool Landscape
| Tool | Purpose | Speed |
|---|---|---|
| Nuclei | Template-based vulnerability scanning | Fast |
| Nmap | Port/service discovery | Fast |
| SQLMap | SQL injection detection | Slow |
| Nikto | Web server misconfiguration | Medium |
| OWASP ZAP | Full web app scanning | Medium |
| WPScan | WordPress-specific scanning | Medium |
| Trivy | Container/IaC security | Fast |
| Semgrep | SAST (code-level) | Fast |
Nuclei: Template-Based Scanning
Nuclei is the most practical tool for automated pentest pipelines. It uses YAML templates that define what to check and how to detect it. ProjectDiscovery maintains a library of 6,000+ templates covering CVEs, misconfigurations, and default credentials.
Installation:
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Or Docker
docker pull projectdiscovery/nuclei:latestBasic scan:
# Scan with all templates
nuclei -u https://your-app.com -o results.txt
# Scan with specific template categories
nuclei -u https://your-app.com \
-tags owasp,cve,misconfig \
-severity critical,high \
-o results.json -jsonWriting a custom template:
# templates/custom-api-key-exposure.yaml
id: api-key-exposure
info:
name: API Key Exposed in Response
author: security-team
severity: high
tags: custom,secret
requests:
- method: GET
path:
- "{{BaseURL}}/api/config"
- "{{BaseURL}}/config.json"
- "{{BaseURL}}/.env"
matchers-condition: or
matchers:
- type: regex
regex:
- 'api_key\s*[:=]\s*["\']?[A-Za-z0-9]{20,}'
- 'secret_key\s*[:=]\s*["\']?[A-Za-z0-9]{20,}'
condition: ornuclei -u https://your-app.com -t templates/custom-api-key-exposure.yamlCI/CD integration:
# .github/workflows/security.yml
- name: Nuclei Security Scan
run: |
docker run --rm \
-v $(pwd):/reports \
projectdiscovery/nuclei:latest \
-u https://staging.your-app.com \
-severity critical,high \
-tags owasp \
-o /reports/nuclei-results.json \
-json \
-nc # No color (cleaner CI output)
- name: Check for critical findings
run: |
CRITICAL=$(cat nuclei-results.json | jq '[.[] | select(.info.severity == "critical")] | length')
if [ "$CRITICAL" -gt "0" ]; then
echo "FAILED: $CRITICAL critical vulnerabilities found"
cat nuclei-results.json | jq '.[] | select(.info.severity == "critical") | {name: .info.name, url: .matched-at}'
exit 1
fiNmap: Service Discovery
Nmap maps what ports and services are exposed. Run it to verify your attack surface matches expectations.
# Basic scan
nmap -sV -sC --open 192.168.1.100
# Full port scan
nmap -p- -sV 192.168.1.100
# Script scan for common web vulnerabilities
nmap -p 80,443 --script=http-security-headers,http-methods,http-robots.txt your-app.comAutomated expected-vs-actual check:
#!/bin/bash
# Check that only expected ports are open
EXPECTED="22 80 443"
ACTUAL=$(nmap -p- --open -oG - "$TARGET" | grep "Ports:" | grep -oP '\d+/open' | cut -d/ -f1 | sort -n | tr '\n' ' ')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "ALERT: Unexpected open ports: $ACTUAL (expected: $EXPECTED)"
exit 1
fiSQLMap: SQL Injection Testing
SQLMap automates SQL injection detection and exploitation. Use only against systems you own.
# Test a URL parameter
sqlmap -u "https://your-app.com/products?id=1" --batch
# Test a POST form
sqlmap -u "https://your-app.com/login" \
--data="username=test&password=test" \
--batch
# Test with cookies (authenticated)
sqlmap -u "https://your-app.com/api/user" \
--cookie="session=your-session-token" \
--batchCI/CD integration (test SQL injection on staging):
# Only test specific parameters, not full exploitation
sqlmap \
-u "http://staging.your-app.com/search?q=test" \
--level=2 \ # Test depth (1-5, higher = more aggressive)
--risk=1 \ # Payload risk (1-3, keep at 1 for safe testing)
--batch \ # Non-interactive
--no-logging \ # Reduce noise
--forms # Auto-detect and test HTML formsNikto: Web Server Scanning
Nikto scans for web server misconfigurations, outdated software, and default files:
nikto -h https://your-app.com -o nikto-report.html -Format htmlWhat Nikto finds:
- Dangerous HTTP methods (PUT, DELETE enabled)
- Default files (phpinfo.php, wp-login.php, .git/config)
- Missing security headers
- Outdated server software versions
- Directory listings
- Common backdoors and shells
# Scan with authentication
nikto -h https://your-app.com \
-id "admin:password" \ # Basic auth
-Cgidirs all # Scan CGI directoriesBuilding a Pentest Pipeline
Architecture:
Code Push
↓
Unit + Integration Tests
↓
SAST (Semgrep, CodeQL) ← static code analysis
↓
Build + Deploy to Staging
↓
DAST (ZAP, Nuclei) ← dynamic app testing
↓
Infrastructure Scan (Trivy, Nmap) ← environment checks
↓
Security Gate (fail if critical findings)
↓
Deploy to ProductionFull pipeline (GitHub Actions):
name: Security Pipeline
on:
push:
branches: [main]
jobs:
sast:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: p/owasp-top-ten
dependency-scan:
name: Dependency Vulnerabilities
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high
- name: Trivy filesystem scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: CRITICAL,HIGH
exit-code: 1
dast:
name: Dynamic Security Testing
runs-on: ubuntu-latest
needs: [sast]
steps:
- name: Deploy to staging
run: kubectl apply -f k8s/staging/
- name: Wait for deployment
run: |
kubectl rollout status deployment/my-app -n staging --timeout=120s
/usr/local/bin/await 'curl -sf http://staging.my-app.com/health'
- name: Nuclei scan
run: |
docker run --rm projectdiscovery/nuclei:latest \
-u http://staging.my-app.com \
-severity critical,high \
-json -o /tmp/nuclei.json
- name: ZAP baseline scan
uses: zaproxy/action-baseline@v0.11.0
with:
target: 'http://staging.my-app.com'
fail_action: true
security-gate:
name: Security Gate
runs-on: ubuntu-latest
needs: [sast, dast, dependency-scan]
steps:
- name: Check all security jobs passed
run: echo "All security checks passed"Scope Management
Automated pentest tools can cause unintended damage if misdirected. Scope management prevents this.
Define allowed targets:
# pentest-scope.yaml
targets:
- host: staging.your-app.com
ports: [80, 443, 8080]
paths:
- /api/*
- /public/*
excluded:
- /api/admin/* # Don't test admin endpoints in automation
- host: 10.0.0.0/24 # Internal network range
ports: [22, 80, 443, 5432, 6379]
exclude_always:
- production.your-app.com # Never scan production automatically
- third-party-service.com # External services we don't ownValidate scope before running:
#!/bin/bash
TARGET=$1
ALLOWED_TARGETS=("staging.your-app.com" "10.0.0.0/24")
is_in_scope() {
for allowed in "${ALLOWED_TARGETS[@]}"; do
if [[ "$TARGET" == *"$allowed"* ]]; then
return 0
fi
done
return 1
}
if ! is_in_scope "$TARGET"; then
echo "ERROR: $TARGET is not in scope. Aborting."
exit 1
fi
echo "Target $TARGET is in scope. Proceeding with scan."Rate Limiting and Being a Good Scanner
Automated scanners generate significant traffic. Protect your staging environment:
# Nuclei rate limiting
nuclei -u https://staging.your-app.com \
-rate-limit 50 \ # 50 requests per second
-bulk-size 10 \ # 10 concurrent requests
-timeout 10 # 10 second timeout per request
# SQLMap rate limiting
sqlmap -u "http://staging.your-app.com/search?q=1" \
--delay=1 \ # 1 second between requests
--max-threads=5 # Max 5 concurrent connectionsInterpreting Results
False positive rates by tool (approximate):
| Tool | False Positive Rate | Notes |
|---|---|---|
| Nuclei (critical templates) | < 5% | Well-tested templates |
| ZAP (SQL Injection) | ~20% | Needs manual verification |
| ZAP (XSS) | ~30% | Many false positives for reflections |
| Nikto | ~40% | High noise, focus on high-severity only |
| SQLMap | < 10% | More targeted, but slow |
For CI/CD integration:
- Block on: Nuclei critical/high findings, ZAP SQL injection
- Warn on: ZAP medium findings, Nikto
- Review separately: Low severity, informational
Always manually verify critical findings before treating them as confirmed vulnerabilities. A scanner reporting "SQL Injection found" requires a human to confirm it's exploitable, understand the impact, and prioritize the fix.