add and create all learn/ folders for intermediate projects

This commit is contained in:
CarterPerez-dev 2026-02-01 03:33:40 -05:00
parent 218305e722
commit e36d2ffdb0
25 changed files with 3446 additions and 1185 deletions

View File

@ -1,4 +1,4 @@
# ⒸAngelaMos | 2025
# ⒸAngelaMos | 2026
# ---------------------------------------------
# API Security Scanner - Environment Variables
# Copy this file to .env and update the values

View File

@ -1,6 +1,6 @@
"""
AngelaMos | CarterPerez-dev
CertGames.com | 2025
CertGames.com | 2026
----
API Security Scanner

View File

@ -101,3 +101,4 @@ def get_settings() -> Settings:
settings = get_settings()

View File

@ -1,5 +1,5 @@
"""
AngelaMos | 2025
AngelaMos | 2026
FastAPI application factory for main.py
"""
@ -76,3 +76,4 @@ def _register_routes(app: FastAPI) -> None:
app.include_router(auth_router)
app.include_router(scans_router)

View File

@ -1,5 +1,5 @@
"""
CertGames.com | 2025
CertGames.com | 2026
AngelaMos | CarterPerez-dev
----
API Security Scanner FastAPI entry point

View File

@ -1,12 +1,11 @@
[project]
name = "api-security-scanner-backend"
version = "1.0.0"
version = "1.0.1"
description = "Backend API for security testing tool"
requires-python = ">=3.11"
authors = [ # Replace my Name & Email Here
requires-python = ">=3.13"
authors = [
{name = "CarerPerez-dev", email = "support@certgames.com"}
]
readme = "README.md"
license = {text = "MIT"}
dependencies = [
@ -17,7 +16,7 @@ dependencies = [
# Database
"sqlalchemy==2.0.46",
"psycopg2-binary==2.9.11",
"alembic==1.18.1",
"alembic>=1.18.3",
# Security
"slowapi==0.1.9",
"python-jose[cryptography]==3.5.0",
@ -68,7 +67,7 @@ packages = [
]
[tool.ruff]
target-version = "py311"
target-version = "py313"
line-length = 95
indent-width = 4
exclude = [
@ -136,7 +135,7 @@ ignore = [
[tool.mypy]
python_version = "3.11"
python_version = "3.13"
mypy_path = "backend"
namespace_packages = true
explicit_package_bases = true
@ -190,7 +189,7 @@ disable_error_code = [
[tool.pylint.main]
py-version = "3.11"
py-version = "3.13"
jobs = 4
load-plugins = [
"pylint_pydantic",

View File

@ -1,4 +1,4 @@
# ⒸAngelaMos | 2025
# ⒸAngelaMos | 2026
services:
# PostgreSQL Database

View File

@ -7,7 +7,7 @@
<meta name="description" content="API Security Scanner Project, React Typescript, Nginx, Docker, FastAPI, Python, Pydandic, OWASP">
<meta name="author" content="Carter Perez">
<meta property="og:locale" content="en_US">
<meta property="article:author" content="Carterperez-dev - REPLACE MY NAME">
<meta property="article:author" content="Carterperez-dev">
<meta name="keywords" content="scanner, security, owasp, api scanner, api security, cybersecurity, projects, project">
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">

View File

@ -1,6 +1,6 @@
{
"name": "api-security-scanner",
"version": "1.0.0",
"version": "1.0.1",
"private": true,
"type": "module",
"scripts": {

View File

@ -1,284 +0,0 @@
# How The Scanner Is Built
This doc explains the architecture decisions. Not how to use it, but why it works the way it does.
## The Layer Cake
The application follows a standard layered architecture.
```
┌─────────────────────────────────────┐
│ Routes (API) │ FastAPI endpoints
├─────────────────────────────────────┤
│ Services │ Business logic
├─────────────────────────────────────┤
│ Repositories / Scanners │ Data access / Scanning
├─────────────────────────────────────┤
│ Models │ SQLAlchemy ORM
├─────────────────────────────────────┤
│ Database │ PostgreSQL
└─────────────────────────────────────┘
```
**Routes** handle HTTP requests, validate input with Pydantic, and return responses. They do not contain business logic.
**Services** orchestrate operations. ScanService coordinates which scanners to run and saves results. AuthService handles user registration and login.
**Repositories** abstract database operations. This keeps SQL queries out of services and makes testing easier.
**Scanners** are the security testing engines. Each scanner inherits from BaseScanner and implements a scan() method.
## The Scanner Pattern
All scanners inherit from BaseScanner which provides common functionality.
```python
class BaseScanner(ABC):
def __init__(self, target_url, auth_token, max_requests):
self.target_url = target_url
self.session = self._create_session()
# ...
def make_request(self, method, endpoint, **kwargs):
# Rate limiting, retries, timing
pass
@abstractmethod
def scan(self) -> TestResultCreate:
# Implemented by each scanner
pass
```
This design means:
- Common HTTP logic lives in one place
- Request spacing and retry logic is consistent
- Each scanner focuses only on its detection logic
- Adding a new scanner is straightforward
### Request Spacing
Scanners do not blast requests at targets. Each request is spaced to avoid overwhelming the target or triggering defensive rate limits.
```python
required_delay = 1.0 / (max_requests / window_seconds)
```
With default settings of 100 requests per 60 second window, that is about 600ms between requests. Random jitter is added to avoid predictable patterns.
This matters because:
1. You do not want to DoS your own production systems during testing
2. Aggressive scanning triggers alerts and gets you blocked
3. Some timing attacks need consistent baseline measurements
### Retry Logic
Requests that fail get retried with exponential backoff.
```python
retry_count = 0
backoff_factor = 2.0
while retry_count <= max_retries:
response = session.request(method, url)
if response.status_code == 429:
wait_time = int(response.headers.get("Retry-After", default_wait))
time.sleep(wait_time)
retry_count += 1
continue
if response.status_code >= 500:
wait_time = backoff_factor ** retry_count
time.sleep(wait_time)
retry_count += 1
continue
return response
```
429 responses respect the Retry-After header. 5xx errors trigger exponential backoff. This keeps the scanner resilient against temporary failures.
## Evidence Collection
Every scan result includes evidence. This is not just for debugging. Proper evidence is required for professional security reports.
```python
evidence = {
"status_code": response.status_code,
"response_time_ms": elapsed * 1000,
"response_length": len(response.text),
"headers": self._redact_sensitive_headers(dict(response.headers)),
"payload": str(payload),
}
```
Sensitive headers are automatically redacted. You do not want authorization tokens showing up in scan reports.
```python
sensitive_headers = [
"authorization",
"cookie",
"x-api-key",
"x-auth-token",
]
```
## The Service Layer
ScanService coordinates scans. It maps test types to scanner classes and handles the workflow.
```python
scanner_mapping = {
TestType.RATE_LIMIT: RateLimitScanner,
TestType.AUTH: AuthScanner,
TestType.SQLI: SQLiScanner,
TestType.IDOR: IDORScanner,
}
for test_type in scan_request.tests_to_run:
scanner_class = scanner_mapping.get(test_type)
scanner = scanner_class(target_url, auth_token, max_requests)
result = scanner.scan()
results.append(result)
```
If a scanner throws an exception, the service catches it and creates an error result. One failing scanner does not kill the entire scan.
```python
except Exception as e:
results.append(
TestResultCreate(
test_name=test_type,
status="error",
details=f"Scanner error: {str(e)}",
)
)
```
## Database Design
Three main tables.
**users**: Account information. Passwords are bcrypt hashed.
**scans**: Metadata about each scan (who ran it, when, target URL).
**test_results**: Individual test outcomes linked to scans.
```
users (1) ──── (*) scans (1) ──── (*) test_results
```
Cascade deletes are configured so deleting a user removes their scans, and deleting a scan removes its test results.
## Rate Limiting The Scanner API
The scanner API itself is rate limited using slowapi.
```python
limiter = Limiter(key_func=get_remote_address)
@router.post("/")
@limiter.limit("5/minute")
async def create_scan(...):
pass
```
Running security scans is expensive. Without rate limiting, someone could hammer your scanner with requests and either run up your bills or use it to attack third parties.
The scan endpoint has stricter limits than read endpoints. Creating a scan triggers potentially hundreds of requests to the target.
## Authentication Flow
JWT tokens with bcrypt password hashing.
```
1. User registers with email/password
2. Password is hashed with bcrypt (cost factor 12)
3. User logs in with credentials
4. Server validates password against hash
5. Server issues JWT with user ID and expiration
6. Client sends JWT in Authorization header
7. Server validates JWT on each request
```
The JWT contains minimal claims. Just enough to identify the user.
```python
{
"sub": "user_id",
"exp": expiration_timestamp
}
```
No roles or permissions in the token. Those are checked against the database on each request. This means you can revoke permissions instantly without waiting for token expiration.
## Error Handling Strategy
The application uses HTTPException for expected errors and lets unexpected errors bubble up.
```python
if not scan:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Scan not found",
)
if scan.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this scan",
)
```
FastAPI catches HTTPException and returns proper JSON error responses. Unhandled exceptions return 500 with minimal details (no stack traces in production).
## Configuration
Settings are loaded from environment variables with pydantic_settings.
```python
class Settings(BaseSettings):
BACKEND_HOST: str = "0.0.0.0"
BACKEND_PORT: int = 8000
DEBUG: bool = False
DATABASE_URL: str
JWT_SECRET_KEY: str
# ...
model_config = SettingsConfigDict(env_file=".env")
```
Defaults exist for development convenience but secrets like JWT_SECRET_KEY have no default. The application fails fast if required settings are missing.
## Why This Structure
The layered approach might seem like overkill for a scanner, but it pays off.
**Testing**: Each layer can be tested independently. Mock the repository to test services. Mock services to test routes.
**Extensibility**: Adding a new scanner means creating one file that inherits from BaseScanner. No changes to services or routes needed.
**Maintainability**: Database logic stays in repositories. HTTP logic stays in routes. Business logic stays in services. Changes are localized.
**Security**: Separation of concerns makes security review easier. Auth checks happen in one place. Input validation happens in one place.
## Adding A New Scanner
To add a new vulnerability scanner:
1. Create a new file in `scanners/`
2. Inherit from BaseScanner
3. Implement the scan() method
4. Add payloads to payloads.py if needed
5. Add the test type to core/enums.py
6. Add mapping in ScanService
```python
class XSSScanner(BaseScanner):
def scan(self) -> TestResultCreate:
# Detection logic here
pass
```
The scanner pattern handles HTTP requests, retries, and evidence collection. You focus on the detection logic.

View File

@ -1,318 +0,0 @@
# How Security Scanning Actually Works
This doc covers the techniques behind API security scanning. How detection works, what makes a good payload, and the tradeoffs between thoroughness and noise.
## The Scanning Process
Every scanner follows the same general flow.
```
1. Establish baseline (normal behavior)
2. Send test payloads
3. Observe differences from baseline
4. Classify findings
5. Collect evidence
```
The key insight: vulnerability detection is about finding anomalies. Normal requests produce normal responses. Malicious requests produce different responses. The difference reveals the vulnerability.
## Baseline Timing
Time based attacks need accurate baselines. Network latency varies. Server response time varies. You need to distinguish between normal variance and injected delays.
```python
def get_baseline_timing(endpoint, samples=5):
times = []
for _ in range(samples):
response = make_request("GET", endpoint)
times.append(response.elapsed)
time.sleep(0.5)
return mean(times), stdev(times)
```
Taking multiple samples and calculating standard deviation lets you set a proper threshold.
```python
threshold = baseline_mean + (3 * baseline_stdev)
```
If your baseline is 200ms with a standard deviation of 50ms, your threshold is 350ms. A response taking 5.2 seconds when you injected a 5 second delay is clearly the attack working, not network issues.
## Payload Design
Good payloads are specific. They test one thing and produce a clear signal.
### SQL Injection Payloads
Basic auth bypass payloads:
```
' OR '1'='1
' OR 1=1--
admin'--
```
These work against string fields in WHERE clauses. The quote breaks out of the value, the OR makes the condition always true, and the comment (-- or #) ignores the rest of the query.
Time delay payloads vary by database:
```
'; WAITFOR DELAY '0:0:5'-- (MSSQL)
'; SELECT SLEEP(5)-- (MySQL)
'; pg_sleep(5)-- (PostgreSQL)
```
Each database has different syntax for delays. The scanner tries multiple variants to identify which database is in use.
Union payloads for data extraction:
```
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT username,password FROM users--
```
The number of NULLs must match the column count of the original query. Scanners typically try 1, 2, 3, etc. until one works.
### Authentication Payloads
JWT none algorithm variants:
```python
["none", "None", "NONE", "nOnE", "NoNe"]
```
Case variations catch libraries that only check for lowercase "none" but accept mixed case.
Invalid tokens to test error handling:
```python
["", "invalid", "null", "undefined", "' OR '1'='1"]
```
If any of these get a 200 response, the token validation is broken.
### Rate Limit Bypass Headers
```python
[
{"X-Forwarded-For": "127.0.0.1"},
{"X-Real-IP": "10.0.0.1"},
{"X-Originating-IP": "192.168.1.1"},
{"X-Client-IP": "8.8.8.8"},
{"CF-Connecting-IP": "1.1.1.1"},
{"True-Client-IP": "172.16.0.1"},
]
```
Each header is tested with rotating IP values. If requests with different spoofed IPs all succeed while requests without the header get blocked, the rate limiter trusts that header.
## Response Analysis
Different vulnerabilities produce different signals.
### Error Messages
SQL injection often produces database errors.
```python
error_signatures = {
"mysql": ["sql syntax", "mysql_fetch", "warning: mysql"],
"postgres": ["pg_query", "pg_exec", "pgsql"],
"mssql": ["odbc sql server", "sqlexception"],
"oracle": ["ora-", "pl/sql"],
}
for db_type, signatures in error_signatures.items():
for signature in signatures:
if signature in response.text.lower():
return {"vulnerable": True, "database_type": db_type}
```
The error message also reveals the database type, which informs which payloads to try next.
### Response Length Differences
Boolean based blind SQLi detection relies on response length.
```python
true_response = make_request("GET", "/?id=1 AND 1=1")
false_response = make_request("GET", "/?id=1 AND 1=2")
length_diff = abs(len(true_response.text) - len(false_response.text))
if length_diff > 100:
return {"vulnerable": True, "confidence": "HIGH" if length_diff > 500 else "MEDIUM"}
```
A large difference means the database is evaluating the boolean condition. The query structure is being modified by the input.
### Status Codes
Status codes reveal a lot.
| Code | Meaning for Testing |
|------|---------------------|
| 200 | Request succeeded, possibly vulnerable |
| 401 | Authentication required (expected for auth tests) |
| 403 | Forbidden (authorization working) |
| 404 | Resource not found (might be good for IDOR) |
| 429 | Rate limited (rate limiter working) |
| 500 | Server error (might indicate crash from payload) |
A 200 on an endpoint that should require auth is a vulnerability. A 429 when testing rate limits means they work. A 500 from a SQL payload might indicate injection but also might just be a crash.
### Timing
Timing analysis requires statistical rigor.
```python
baseline_mean, baseline_stdev = get_baseline_timing("/")
delay_seconds = 5
expected_delay = baseline_mean + delay_seconds
for payload in time_based_payloads:
times = []
for _ in range(3): # Multiple samples per payload
response = make_request("GET", f"/?id={payload}")
times.append(response.elapsed)
time.sleep(1)
avg_time = mean(times)
if avg_time >= expected_delay - 1: # Allow 1 second tolerance
return {"vulnerable": True}
```
Three samples per payload reduces false positives from network hiccups. The tolerance accounts for timing imprecision.
## False Positives and Confidence
Not every anomaly is a vulnerability. Scanners need to distinguish real findings from noise.
### High Confidence Indicators
- Database error strings in response (SQLi)
- Token with "none" algorithm accepted (JWT)
- 5+ second response when 5 second delay injected (Time based SQLi)
- Different IDs accessible with same auth token (IDOR)
### Lower Confidence Indicators
- Response length differences (might be caching)
- Single timing anomaly (network variance)
- 500 errors (might be unrelated crash)
The scanner assigns confidence levels based on how definitive the evidence is.
```python
if response_time >= expected_delay:
confidence = "HIGH"
elif response_time >= expected_delay * 0.8:
confidence = "MEDIUM"
```
## Scanning Safely
Security scanning can break things. These practices minimize damage.
### Request Spacing
Never send requests as fast as possible.
```python
required_delay = 1.0 / (max_requests / window_seconds)
time.sleep(required_delay + random_jitter)
```
This protects:
- Target servers from being overwhelmed
- Your scanner from being blocked
- Timing measurements from being skewed
### Read Only Operations
The scanner uses GET requests where possible and avoids destructive payloads.
```python
# Testing payloads
"' OR '1'='1" # Yes: tests injection without modification
"'; DROP TABLE--" # Included but dangerous on real data
```
Even though the payloads include statements like DROP TABLE, the scanner is testing for the vulnerability, not exploiting it. If the database is vulnerable, the SELECT from the OR clause would work anyway.
### Authentication Scope
When testing authenticated endpoints, the scanner uses the provided token consistently.
```python
if self.auth_token:
session.headers.update({"Authorization": f"Bearer {self.auth_token}"})
```
This means:
- Tests stay within authorized scope
- IDOR tests check if you can access other users' data, not if you can bypass auth entirely
- Results are relevant to your access level
### Error Recovery
Failed requests do not crash the scan.
```python
try:
result = scanner.scan()
except Exception as e:
result = TestResultCreate(
status="error",
details=f"Scanner error: {str(e)}",
)
```
Network timeouts, connection resets, and unexpected responses are handled. The scan continues with remaining tests.
## Interpreting Results
Scanner output needs human interpretation.
### Vulnerable Status
The scanner found evidence of a vulnerability. Review the evidence to confirm:
- Is the payload visible in the evidence?
- Does the detection method match the vulnerability type?
- Could this be a false positive?
### Safe Status
No vulnerability detected with the payloads tested. This does not mean the target is secure. It means these specific tests passed.
```
"No SQL injection vulnerabilities detected"
What this means: Standard SQLi payloads did not trigger errors or timing anomalies
What this does NOT mean: The application is immune to SQL injection
```
Different payloads, different endpoints, or different parameters might reveal vulnerabilities the scan missed.
### Error Status
The test could not complete. Check the error message:
- Connection refused: Target is down or blocking your IP
- Timeout: Target is slow or has aggressive rate limiting
- SSL error: Certificate issues
Errors mean you need to investigate before trusting the result.
## Limitations
No scanner finds everything. Understanding limitations helps you use results appropriately.
**Coverage**: The scanner tests a predefined set of payloads and techniques. Novel attack vectors are not covered.
**Depth**: Automated scanning is shallow compared to manual testing. Complex logic flaws require human analysis.
**Context**: The scanner does not understand your business logic. It cannot tell if a vulnerability matters for your specific application.
**Evasion**: Sophisticated applications might detect and block scanning patterns. Low and slow scanning helps but is not foolproof.
**False Negatives**: Passing all tests does not mean the application is secure. It means these specific tests passed against these specific endpoints with these specific inputs.
Use automated scanning as one layer of security testing, not the only layer. Combine with code review, manual penetration testing, and ongoing monitoring.

View File

@ -1,224 +0,0 @@
# API Vulnerabilities This Scanner Actually Detects
This scanner tests for four categories of vulnerabilities. Each one maps to real OWASP API Security Top 10 entries. Here is what they are, how attackers exploit them, and why they matter.
## Quick Reference
| Vulnerability | OWASP ID | Severity | What Happens |
|---------------|----------|----------|--------------|
| IDOR/BOLA | API1:2023 | High | Attacker accesses other users data |
| Broken Auth | API2:2023 | Critical | Attacker bypasses login entirely |
| SQL Injection | API8:2023 | Critical | Attacker reads/modifies your database |
| Missing Rate Limits | API4:2023 | High | Attacker brute forces or DoS attacks |
## IDOR/BOLA (Broken Object Level Authorization)
OWASP ranks this number one for a reason. It is the most common API vulnerability in the wild.
The problem: your API checks if a user is logged in, but not if they own the resource they are requesting.
```
GET /api/users/42/orders
User 42: sees their orders (correct)
User 99: also sees user 42's orders (vulnerable)
```
The scanner tests this by:
1. Extracting IDs from API responses (both numeric and UUIDs)
2. Trying to access resources with manipulated IDs
3. Checking if sequential IDs are predictable
Real world example: In 2019, First American Financial exposed 885 million records because their document URLs used sequential IDs with no authorization check. Change the number in the URL, see someone else's mortgage documents.
### Why UUIDs Do Not Fix This
Some developers think switching from numeric IDs to UUIDs solves IDOR. It does not.
UUIDs make enumeration harder, not impossible. If an attacker finds one UUID (from a shared link, email, logs, or browser history), they can still access that resource without authorization.
```
GET /api/documents/550e8400-e29b-41d4-a716-446655440000
```
The fix is always authorization checks. Verify the requesting user has permission to access the specific resource, regardless of ID format.
## Broken Authentication (API2:2023)
Authentication vulnerabilities let attackers log in as other users or bypass authentication entirely.
### Missing Authentication
The most obvious case: endpoints that should require login but do not.
```python
# Vulnerable: no auth required
@app.get("/api/admin/users")
def get_all_users():
return db.query(User).all()
```
The scanner tests this by making requests without any authentication headers and checking if it gets a 200 response instead of 401.
### JWT Vulnerabilities
JWTs have specific implementation bugs that keep appearing.
**The None Algorithm Attack**
JWTs have three parts: header, payload, signature. The header specifies which algorithm verifies the signature. Some libraries accept "none" as a valid algorithm, meaning no signature required.
```
Original header: {"alg": "HS256", "typ": "JWT"}
Malicious header: {"alg": "none", "typ": "JWT"}
```
If the server accepts this, an attacker can forge any token they want. The scanner tests multiple case variations because some libraries only check for lowercase "none".
```python
# These all bypass poorly implemented JWT validation
"none", "None", "NONE", "nOnE"
```
**Signature Removal**
Related to the none algorithm: some implementations check if the algorithm is "none" but still accept tokens with empty signatures.
```
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.
^ empty signature
```
The scanner removes signatures from valid tokens and tests if the server still accepts them.
### Invalid Token Handling
Good APIs reject malformed tokens. Bad APIs might accept them or crash.
The scanner sends garbage tokens and checks for 200 responses:
- Empty strings
- Random characters
- SQL injection payloads in tokens
- Path traversal strings
If any of these return 200, something is wrong with your token validation.
## SQL Injection (API8:2023)
SQL injection has been around for 25 years and still makes the top 10 because developers keep building queries with string concatenation.
### Error Based Detection
The easiest SQLi to find. Send a malformed query, get a database error in the response.
```
GET /api/users?id=' OR '1'='1
Response: "You have an error in your SQL syntax near..."
```
The scanner looks for error signatures from MySQL, PostgreSQL, MSSQL, and Oracle. Each database has distinctive error messages.
| Database | Error Contains |
|----------|----------------|
| MySQL | "mysql_fetch", "sql syntax", "warning: mysql" |
| PostgreSQL | "pg_query", "pg_exec", "pgsql" |
| MSSQL | "odbc sql server", "sqlexception" |
| Oracle | "ora-", "pl/sql" |
Finding these errors means two things: the input reaches the database unsanitized, and error messages are exposed to users. Both are bad.
### Boolean Based Blind
When errors are hidden, you can still detect SQLi by comparing responses.
```
GET /api/users?id=1 AND 1=1 (true condition)
GET /api/users?id=1 AND 1=2 (false condition)
```
If the true condition returns normal results and the false condition returns empty or different results, the input is being evaluated as SQL.
The scanner measures response length differences. A significant difference (over 100 bytes typically) between true and false conditions indicates a vulnerability.
### Time Based Blind
The hardest to detect but most reliable for confirming SQLi.
```
GET /api/users?id=1; WAITFOR DELAY '0:0:5'-- (MSSQL)
GET /api/users?id=1'; SELECT SLEEP(5)-- (MySQL)
GET /api/users?id=1'; pg_sleep(5)-- (PostgreSQL)
```
If the response takes 5 seconds longer than normal, the database executed the delay. This works even when the application shows no visible difference in output.
The scanner:
1. Establishes baseline response times with multiple samples
2. Calculates standard deviation to account for network variance
3. Sends delay payloads and measures actual response time
4. Compares against expected delay time (baseline + injected delay)
False positives are rare with time based testing because network latency does not consistently add exactly 5 seconds.
## Rate Limiting (API4:2023)
APIs without rate limiting are vulnerable to brute force attacks, credential stuffing, and denial of service.
### Detection
The scanner makes multiple requests and looks for:
1. Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, etc.)
2. 429 Too Many Requests responses
3. Retry-After headers
Finding rate limit headers but never hitting 429 means the limits exist in name only. The scanner flags this as "headers only" which is a medium severity issue.
### Bypass Testing
Rate limiters often have implementation bugs.
**IP Header Spoofing**
Many rate limiters use client IP for tracking. If they trust X-Forwarded-For or similar headers, attackers can bypass limits by rotating fake IPs.
```
X-Forwarded-For: 10.0.0.1
X-Forwarded-For: 10.0.0.2
X-Forwarded-For: 10.0.0.3
```
The scanner tests X-Forwarded-For, X-Real-IP, X-Client-IP, X-Originating-IP, CF-Connecting-IP, and True-Client-IP. If any of these bypass rate limits, attackers can make unlimited requests.
**Endpoint Variations**
Some rate limiters are case sensitive or miss URL variations.
```
/api/login (rate limited)
/API/LOGIN (not rate limited?)
/api/login/ (not rate limited?)
/api/./login (not rate limited?)
```
The scanner tests path variations including case changes, trailing slashes, and encoded characters.
## What The Severity Ratings Mean
The scanner uses five severity levels.
**Critical**: Immediate risk of data breach or system compromise. SQL injection and JWT bypass fall here. Fix before anything else.
**High**: Serious risk that needs prompt attention. IDOR and missing rate limits on auth endpoints. Attackers will find and exploit these.
**Medium**: Moderate risk. Rate limit headers without enforcement, or predictable ID patterns. Still needs fixing, but less urgent.
**Low**: Minor issues. Might indicate poor practices but no immediate exploit path.
**Info**: Informational findings. The test passed or found something worth noting.
## Further Reading
- OWASP API Security Top 10 2023: https://owasp.org/API-Security/
- PortSwigger Web Security Academy: https://portswigger.net/web-security
- HackTricks API Pentesting: https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/api-pentesting.html

View File

@ -1,10 +1,10 @@
{
"name": "api-security-scanner",
"version": "1.0.0",
"version": "1.0.1",
"description": "Automated API security testing tool with FastAPI backend and ReactTS frontend",
"private": true,
"scripts": {
"dev": "docker-compose -f docker-compose.dev.yml up",
"dev": "docker compose -f docker-compose.dev.yml up",
"dev:build": "docker compose -f docker-compose.dev.yml up --build",
"dev:down": "docker compose -f docker-compose.dev.yml down",
"dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
@ -17,7 +17,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/carterperez-dev/api-security-scanner"
"url": "https://github.com/CarterPerez-dev/Cybersecurity-Projects/PROJECTS/intermediate/api-security-scanner"
},
"keywords": [
"security",
@ -28,6 +28,6 @@
"react",
"docker"
],
"author": "CarerPerez-dev - also replace my github url username with yours",
"author": "https://github.com/CarterPerez-dev",
"license": "MIT"
}

View File

@ -9,7 +9,186 @@ cmd/docksec/ Entry point and CLI commands
internal/
analyzer/ Security checks for different target types
benchmark/ CIS Docker Benchmark control definitions
config/ Runtime configuration and constants
config/ Runtime configuration and constants# Docker Security Audit Tool (docksec)
## What This Is
A Go-based CLI tool that scans Docker environments for security misconfigurations and validates them against the CIS Docker Benchmark. It analyzes running containers, daemon settings, images, Dockerfiles, and docker-compose files to identify vulnerabilities like privileged containers, dangerous capabilities, Docker socket mounts, and hardcoded secrets.
## Why This Matters
Docker containers don't provide security by default. A single misconfiguration can give an attacker complete control over your host system. This tool catches those mistakes before they become breaches.
**Real world scenarios where this applies:**
- **Container escape via Docker socket mount**: In 2018, Tesla's Kubernetes cluster was compromised when attackers found a pod with `/var/run/docker.sock` mounted, letting them escape to the host and mine cryptocurrency.
- **Privilege escalation through capabilities**: The 2014 "Shocker" exploit used `CAP_DAC_READ_SEARCH` to read arbitrary files from the host filesystem, bypassing container isolation completely.
- **Secret exposure in images**: In 2019, over 100,000 Docker images on Docker Hub contained embedded API keys, passwords, and private keys discoverable through simple text searches.
## What You'll Learn
This project teaches you how container security actually works under the hood. By building and extending it, you'll understand:
**Security Concepts:**
- **CIS Docker Benchmark compliance** - industry standard security controls covering host config, daemon settings, images, and runtime. You'll learn which of the 100+ controls matter most and why.
- **Linux capabilities model** - how the 41 discrete privileges work, which ones enable container escape (like `CAP_SYS_ADMIN` and `CAP_SYS_PTRACE`), and how to audit them programmatically.
- **Namespace isolation boundaries** - what PID, network, IPC, and mount namespaces protect against, and how sharing host namespaces breaks that protection.
- **Security profiles (seccomp, AppArmor, SELinux)** - how syscall filtering and mandatory access control actually prevent attacks, not just theoretical defense-in-depth concepts.
- **Secret detection techniques** - pattern matching with regex, entropy analysis using Shannon entropy, and why both are needed to catch different types of leaked credentials.
**Technical Skills:**
- **Docker API interaction** - using the official Docker client library to introspect containers, images, and daemon configuration without shell commands.
- **Static analysis of build files** - parsing Dockerfiles with AST (abstract syntax tree) to detect security issues at build time, before images ever run.
- **Concurrent scanning** - using Go's errgroup and rate limiters to scan hundreds of containers efficiently without overwhelming the daemon.
- **Security reporting formats** - generating SARIF (Static Analysis Results Interchange Format) for GitHub Security, JUnit for CI/CD, and structured JSON for automation.
**Tools and Techniques:**
- **moby/buildkit parser** - the same parser Docker uses to understand Dockerfile syntax, giving you access to instruction metadata and line numbers for precise findings.
- **CIS Benchmark mapping** - translating security controls into automated checks, including determining which findings are "scored" vs "not scored" in compliance reporting.
- **YAML node traversal** - navigating docker-compose files as structured data rather than text, handling both mapping and sequence formats for the same logical configuration.
## Prerequisites
Before starting, you should understand:
**Required knowledge:**
- **Go basics** - structs, interfaces, goroutines, channels. You'll be reading and modifying Go code that uses errgroups for concurrency and rate limiters for API throttling.
- **Docker fundamentals** - images vs containers, how volumes work, what ports do. You need to know the difference between `docker run --privileged` and `--cap-add=NET_ADMIN`.
- **Linux security model** - what root can do, basic permission concepts, why running as UID 0 is dangerous. Understanding `/proc` and `/sys` helps but isn't required.
- **Command line proficiency** - comfortable with `docker inspect`, reading JSON output, understanding exit codes and stderr vs stdout.
**Tools you'll need:**
- **Go 1.23+** - the project uses generics and newer error handling patterns
- **Docker Engine** - running locally or accessible via `DOCKER_HOST`. Docker Desktop on Mac/Windows works fine.
- **Make** (optional) - for running build and test commands conveniently
- **Git** - to clone the repository and track your changes
**Helpful but not required:**
- Experience with security scanning tools like Trivy, Snyk, or Anchore
- Familiarity with the CIS Benchmark PDFs (the tool teaches you the controls)
- Knowledge of Go testing and benchmarking
## Quick Start
Get the project running locally:
```bash
# Clone and navigate
cd PROJECTS/intermediate/docker-security-audit
# Build the binary
go build -o docksec ./cmd/docksec
# Scan all running containers
./docksec scan
# Scan specific targets
./docksec scan --target containers
./docksec scan --target daemon
./docksec scan --target images
# Scan a Dockerfile
./docksec scan --file Dockerfile
# Scan a docker-compose file
./docksec scan --file docker-compose.yml
# Generate JSON output for automation
./docksec scan --output json --output-file results.json
# Generate SARIF for GitHub Security
./docksec scan --output sarif --output-file results.sarif
# Filter by severity
./docksec scan --severity high,critical
# Fail CI builds on findings
./docksec scan --fail-on medium
```
Expected output: Terminal report grouped by category (Container Runtime, Docker Daemon, etc.) showing findings with severity levels, CIS control IDs, descriptions, and remediation steps. Zero findings if your Docker environment is properly secured.
## Project Structure
```
docker-security-audit/
├── cmd/
│ └── docksec/
│ └── main.go # CLI entry point, Cobra commands
├── internal/
│ ├── analyzer/
│ │ ├── analyzer.go # Analyzer interface
│ │ ├── container.go # Running container checks
│ │ ├── daemon.go # Docker daemon config checks
│ │ ├── image.go # Image metadata checks
│ │ ├── dockerfile.go # Dockerfile static analysis
│ │ └── compose.go # docker-compose.yml checks
│ ├── benchmark/
│ │ └── controls.go # CIS Docker Benchmark control registry
│ ├── config/
│ │ ├── config.go # Configuration struct and filters
│ │ └── constants.go # Rate limits, timeouts, thresholds
│ ├── docker/
│ │ └── client.go # Docker API client wrapper
│ ├── finding/
│ │ └── finding.go # Finding data model and collection
│ ├── proc/
│ │ ├── capabilities.go # Linux capabilities parsing
│ │ ├── proc.go # /proc filesystem reading
│ │ └── security.go # Security profile inspection
│ ├── report/
│ │ ├── reporter.go # Reporter interface and factory
│ │ ├── terminal.go # Human-readable output
│ │ ├── json.go # Structured JSON output
│ │ ├── sarif.go # SARIF 2.1.0 format
│ │ └── junit.go # JUnit XML for CI/CD
│ ├── rules/
│ │ ├── capabilities.go # 41 Linux capabilities with risk levels
│ │ ├── paths.go # 200+ sensitive host paths
│ │ └── secrets.go # 80+ secret patterns and entropy
│ └── scanner/
│ └── scanner.go # Main scan orchestration
├── Dockerfile # Multi-stage build for container deployment
├── go.mod # Dependencies
└── go.sum
```
## Next Steps
1. **Understand the concepts** - Read [01-CONCEPTS.md](./01-CONCEPTS.md) to learn how Linux capabilities, namespaces, and security profiles actually work at the kernel level.
2. **Study the architecture** - Read [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) to see how the scanner orchestrates concurrent analyzers, applies rules, and generates findings.
3. **Walk through the code** - Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for detailed implementation walkthroughs with actual code from the project.
4. **Extend the project** - Read [04-CHALLENGES.md](./04-CHALLENGES.md) for specific ideas to add features like runtime monitoring, Kubernetes integration, and custom policies.
## Common Issues
**Docker daemon not accessible**
```
Error: docker daemon not accessible: Cannot connect to the Docker daemon
```
Solution: Check that Docker is running (`docker ps` works), your user is in the `docker` group, or set `DOCKER_HOST` if using remote Docker.
**No findings on known vulnerable containers**
Solution: Check your `--severity` and `--target` flags. The default scans all targets at all severities. Use `--verbose` to see which analyzers run.
**Build fails with "module not found"**
Solution: Run `go mod download` to fetch dependencies. Make sure you're using Go 1.23 or later (`go version`).
**SARIF file not showing in GitHub Security**
Solution: Upload it in a GitHub Actions workflow using `github/codeql-action/upload-sarif@v2`. SARIF files don't auto-upload, they need explicit CI integration.
## Related Projects
If you found this interesting, check out:
- **Trivy** - comprehensive vulnerability scanner that includes configuration scanning. This project focuses specifically on runtime and build-time Docker security.
- **Falco** - runtime security monitoring using eBPF. Complements this tool by detecting threats during execution.
- **Docker Bench Security** - official Docker security checker. This project improves on it with structured output, CI/CD integration, and programmatic access.
docker/ Docker SDK wrapper
finding/ The Finding type and severity levels
parser/ Dockerfile and compose file parsers

View File

@ -0,0 +1,553 @@
# Core Security Concepts
This document explains the security concepts you'll encounter while building this project. These aren't just definitions. We'll dig into why they matter and how they actually work.
## Linux Capabilities
### What It Is
Linux capabilities split the monolithic root privilege into 41 discrete permissions. Instead of checking "is this process root?", the kernel checks "does this process have CAP_NET_ADMIN?" before allowing network configuration changes.
The capability model was introduced in kernel 2.2 (1999) to enable privilege separation. A web server binding to port 80 only needs `CAP_NET_BIND_SERVICE`, not full root access. A backup program only needs `CAP_DAC_READ_SEARCH` to read protected files.
### Why It Matters
Containers run with a default set of capabilities that enable common operations. Docker drops 13 dangerous capabilities by default but still grants 14 others. Adding back dropped capabilities or granting new ones can enable complete container escape.
When you run `docker run --privileged`, Docker grants all 41 capabilities. This is equivalent to running as root on the host, bypassing every container isolation mechanism.
### How It Works
Capabilities are stored as 64-bit bitmasks in the process credential structure. The kernel checks these bits before privileged operations. Here's how this project decodes them from `/proc/PID/status`:
```go
// internal/proc/capabilities.go:87-112
func (c *CapabilitySet) HasCapability(name string) bool {
bit, ok := capabilityNames[name]
if !ok {
return false
}
return (c.Effective & (1 << bit)) != 0
}
```
The `Effective` set determines what the process can currently do. Other sets (`Permitted`, `Inheritable`, `Bounding`, `Ambient`) control capability transitions across exec() and privilege changes.
The project maps all 41 capabilities to severity levels in `internal/rules/capabilities.go:24-323`:
```go
"CAP_SYS_ADMIN": {
Severity: finding.SeverityCritical,
Description: "Perform a range of system administration operations...",
}
```
When scanning containers, the code at `internal/analyzer/container.go:76-102` iterates added capabilities and creates findings for dangerous ones:
```go
for _, cap := range info.HostConfig.CapAdd {
capName := strings.ToUpper(string(cap))
capInfo, exists := rules.GetCapabilityInfo(capName)
if !exists {
continue
}
if capInfo.Severity >= finding.SeverityHigh {
// Create finding with severity from capability database
}
}
```
### Common Attacks
**Container escape via CAP_SYS_ADMIN**
This capability enables mounting filesystems. An attacker can mount the host's root filesystem inside the container:
```bash
# Inside container with CAP_SYS_ADMIN
mkdir /host
mount /dev/sda1 /host
chroot /host
# Now running on the host
```
The Shocker exploit (CVE-2014-6407) used `CAP_DAC_READ_SEARCH` similarly. It could open any file descriptor by path, bypassing namespace isolation.
**Process injection via CAP_SYS_PTRACE**
With this capability, a process can use ptrace() to attach to any other process, read its memory, and inject code:
```c
ptrace(PTRACE_ATTACH, target_pid, NULL, NULL);
ptrace(PTRACE_POKETEXT, target_pid, address, shellcode);
ptrace(PTRACE_SETREGS, target_pid, NULL, &regs);
ptrace(PTRACE_CONT, target_pid, NULL, NULL);
```
An attacker in a container with `CAP_SYS_PTRACE` and `--pid=host` can inject into PID 1 on the host.
**Network manipulation via CAP_NET_ADMIN**
This capability allows modifying routing tables, firewall rules, and network namespaces. An attacker can:
- Redirect traffic destined for other containers
- Create network tunnels to exfiltrate data
- Modify iptables rules to bypass network policies
- Escape to the host network namespace
### Defense Strategies
Drop all capabilities, then add back only what's needed:
```bash
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
```
This project detects when containers add dangerous capabilities. The severity comes from the capability database, not hardcoded logic. Adding a new risky capability just requires updating `internal/rules/capabilities.go`.
## Namespace Isolation
### What It Is
Linux namespaces provide separate views of system resources. Processes in different namespaces see different sets of PIDs, network interfaces, mount points, IPC objects, hostnames, users, and cgroups.
Docker creates 6 namespaces for each container by default:
- **PID namespace** - isolates process IDs
- **Network namespace** - isolates network interfaces
- **Mount namespace** - isolates filesystem mount points
- **IPC namespace** - isolates System V IPC and POSIX message queues
- **UTS namespace** - isolates hostname and domain name
- **User namespace** - isolates user and group IDs (optional, not default)
### Why It Matters
Sharing a namespace with the host breaks that isolation boundary. Running with `--pid=host` lets the container see all host processes. With `CAP_SYS_PTRACE`, it can inject into them.
The most dangerous is `--net=host`, which puts the container on the host network stack. The container can now bind to any host port, sniff all traffic, and reconfigure the host's network.
### How It Works
The kernel maintains a namespace inode for each namespace type. Processes belong to namespaces via these inodes. The `/proc/PID/ns/` directory exposes these as symlinks:
```bash
$ ls -l /proc/self/ns/
lrwxrwxrwx 1 user user 0 Jan 31 12:00 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 user user 0 Jan 31 12:00 net -> 'net:[4026531840]'
```
Containers in different namespaces have different inode numbers. The code at `internal/analyzer/container.go:125-161` checks if containers share host namespaces:
```go
if info.HostConfig.NetworkMode == "host" {
control, _ := benchmark.Get("5.9")
f := finding.New("CIS-5.9", control.Title, finding.SeverityHigh, target)
findings = append(findings, f)
}
```
Kubernetes manifests make this mistake often:
```yaml
spec:
hostNetwork: true # Shares host network namespace
hostPID: true # Shares host PID namespace
containers:
- name: monitor
image: debug-tools
```
### Common Attacks
**Host process discovery via --pid=host**
With the host PID namespace, an attacker can enumerate all host processes, find sensitive services, and identify attack targets:
```bash
# Inside container with --pid=host
ps aux | grep -i password
ls -la /proc/*/environ | xargs grep -a SECRET
```
This exposes process command lines, environment variables, and open file descriptors.
**Network sniffing via --net=host**
On the host network namespace, the container can run packet capture tools to intercept traffic intended for other containers or the host:
```bash
tcpdump -i eth0 -w capture.pcap
# Exfiltrate capture.pcap later
```
This bypasses Docker's network isolation completely.
### Defense Strategies
Never share host namespaces in production. The code at `internal/analyzer/container.go:145-161` flags all shared namespaces:
```go
if info.HostConfig.PidMode == "host" {
// Creates HIGH severity finding for CIS 5.15
}
if info.HostConfig.IpcMode == "host" {
// Creates HIGH severity finding for CIS 5.16
}
```
For debugging, use `docker exec` to run tools in the container's namespaces rather than sharing the host's.
## Security Profiles (Seccomp, AppArmor, SELinux)
### What It Is
Security profiles enforce mandatory access control beyond traditional Unix permissions:
**Seccomp** (Secure Computing Mode) filters system calls using BPF (Berkeley Packet Filter) programs. It can block dangerous syscalls like `ptrace()`, `mount()`, and `reboot()`.
**AppArmor** restricts file access using path-based rules. A profile can allow reading `/etc/nginx/` but deny writing anywhere except `/var/log/nginx/`.
**SELinux** (Security-Enhanced Linux) enforces type-based mandatory access control. It uses security contexts like `system_u:object_r:httpd_sys_content_t:s0` to control access.
### Why It Matters
Without these profiles, a compromised container has full syscall access and can attempt container escape techniques. The default Docker seccomp profile blocks about 44 dangerous syscalls, but containers can run with `--security-opt seccomp=unconfined`.
### How It Works
Docker applies a default seccomp profile unless you override it. The profile is a JSON file defining allowed syscalls:
```json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open"],
"action": "SCMP_ACT_ALLOW"
}
]
}
```
The code at `internal/analyzer/container.go:163-207` checks for missing or disabled profiles:
```go
for _, opt := range info.HostConfig.SecurityOpt {
if opt == "seccomp=unconfined" {
// Creates HIGH severity finding
}
}
```
AppArmor profiles are text files in `/etc/apparmor.d/`:
```
profile docker-default flags=(attach_disconnected,mediate_deleted) {
deny @{PROC}/* w,
deny /sys/[^f]*/** wklx,
capability setuid,
}
```
### Common Pitfalls
**Disabling seccomp for convenience**
Developers disable seccomp when troubleshooting without understanding the risk:
```bash
# Bad: Complete syscall access
docker run --security-opt seccomp=unconfined app
# Good: Custom profile with needed syscalls
docker run --security-opt seccomp=./custom-profile.json app
```
The project detects this at `internal/analyzer/container.go:186-193`.
**Missing profiles entirely**
Older Docker versions or certain configurations don't apply profiles by default. The code checks both for explicit disabling and absence:
```go
if !hasSeccomp && !info.HostConfig.Privileged {
// Creates MEDIUM severity finding
}
```
### Defense Strategies
Always use security profiles in production. Start with Docker's defaults and customize only when needed. The project flags both disabled and missing profiles with different severities (HIGH for explicit disabling, MEDIUM for absence).
## Sensitive Path Mounts
### What It Is
Bind mounts expose host directories and files inside containers. Some paths give full host access if mounted, like `/var/run/docker.sock` (Docker control), `/proc` (process information), and `/` (entire filesystem).
### Why It Matters
The Docker socket is a UNIX socket that accepts Docker API commands. A container with this mounted can create new privileged containers, effectively escaping to the host:
```bash
# Inside container with /var/run/docker.sock mounted
docker run --privileged -v /:/host -it ubuntu chroot /host /bin/bash
# Now on the host
```
This is how the Tesla Kubernetes breach happened.
### How It Works
Docker bind mounts are specified with `-v` or `--mount`. The Docker API includes mount information in container inspection:
```json
{
"Mounts": [
{
"Type": "bind",
"Source": "/var/run/docker.sock",
"Destination": "/var/run/docker.sock"
}
]
}
```
The code at `internal/analyzer/container.go:104-145` checks each mount against a database of 200+ dangerous paths defined in `internal/rules/paths.go`:
```go
for _, mount := range info.Mounts {
source := mount.Source
if rules.IsDockerSocket(source) {
// Creates CRITICAL severity finding
}
if rules.IsSensitivePath(source) {
severity := rules.GetPathSeverity(source)
// Creates finding with path-specific severity
}
}
```
The rules database includes:
- Container runtime sockets (Docker, containerd, CRI-O)
- System directories (`/proc`, `/sys`, `/dev`)
- Configuration directories (`/etc`, `/root`)
- Kubernetes secrets (`/var/lib/kubelet`, `/etc/kubernetes`)
- Cloud provider credentials (`/root/.aws`, `/root/.kube`)
- CI/CD agent data (`/var/lib/jenkins`, `/home/runner`)
Each path has a severity level and description in `internal/rules/paths.go:32-1100`.
### Common Attacks
**Docker socket escape**
This is the most common container escape technique:
```python
# Python script inside container
import docker
client = docker.from_env() # Uses /var/run/docker.sock
# Create privileged container with host filesystem mounted
container = client.containers.run(
'ubuntu',
'chroot /host /bin/bash',
privileged=True,
volumes={'/': {'bind': '/host'}},
detach=True,
stdin_open=True,
tty=True
)
```
**Process memory access via /proc**
Mounting `/proc` exposes host process memory. Reading `/proc/PID/mem` can extract credentials, SSH keys, and other secrets from running processes.
**Kernel manipulation via /sys**
The `/sys` filesystem exposes kernel parameters. Writing to `/sys/kernel/` can disable security features, modify CPU settings, and trigger kernel vulnerabilities.
### Defense Strategies
Never mount the Docker socket unless the container specifically provides Docker-as-a-Service (like CI/CD runners). Even then, use alternatives like Docker's rootless mode or Kaniko for builds.
The project's path database is comprehensive. Adding a new dangerous path just requires an entry in `internal/rules/paths.go`.
## Secret Detection
### What It Is
Secrets are credentials embedded in Dockerfiles, environment variables, or docker-compose files. They include API keys, passwords, private keys, database URLs, and tokens.
### Why It Matters
Secrets in container images are easily discoverable. Anyone who can pull the image can extract the secrets:
```bash
docker history image:tag
docker inspect image:tag
docker run image:tag env
```
In 2019, researchers found over 100,000 Docker Hub images with leaked secrets. Many were active API keys for AWS, GitHub, and other services.
### How It Works
This project uses two techniques for secret detection:
**Pattern matching** with 80+ regular expressions in `internal/rules/secrets.go:29-821`:
```go
{
Type: SecretTypeAWSKey,
Pattern: regexp.MustCompile(`(AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}`),
Description: "AWS Access Key ID",
}
```
**Entropy analysis** using Shannon entropy:
```go
// internal/rules/secrets.go:910-925
func CalculateEntropy(s string) float64 {
freq := make(map[rune]float64)
for _, c := range s {
freq[c]++
}
length := float64(len(s))
var entropy float64
for _, count := range freq {
p := count / length
entropy -= p * math.Log2(p)
}
return entropy
}
```
Secrets typically have high entropy (>4.5 bits per character) because they're random. The code at `internal/analyzer/dockerfile.go:154-213` scans Dockerfiles:
```go
if rules.IsSensitiveEnvName(varName) {
// Check if variable name looks sensitive
}
if rules.IsHighEntropyString(varValue, 16, 4.5) {
// Check if value has high entropy
}
secrets := rules.DetectSecrets(line)
for _, secret := range secrets {
// Create finding for each detected secret type
}
```
### Common Patterns
The project detects:
- AWS keys: `AKIA...`, `aws_secret_access_key=...`
- GitHub tokens: `ghp_...`, `gho_...`
- Google API keys: `AIza...`
- Private keys: `-----BEGIN PRIVATE KEY-----`
- JWTs: `eyJ...` (base64 encoded JSON)
- Database URLs: `postgres://user:pass@host/db`
- Generic patterns: `API_KEY=...`, `PASSWORD=...`
See `internal/rules/secrets.go:29-821` for the complete list.
### Defense Strategies
Use Docker secrets or environment variables at runtime:
```bash
# Bad: Secret in Dockerfile
ENV API_KEY=sk-abc123
# Good: Secret from environment
docker run -e API_KEY=sk-abc123 app
# Better: Docker secrets (Swarm)
echo "sk-abc123" | docker secret create api_key -
docker service create --secret api_key app
```
The project flags both hardcoded secrets and high-entropy strings in `ENV`, `ARG`, and `RUN` instructions.
## CIS Docker Benchmark
### What It Is
The CIS Docker Benchmark is a consensus-driven security configuration guide published by the Center for Internet Security. It provides 100+ controls across 7 sections:
1. Host Configuration
2. Docker Daemon Configuration
3. Docker Daemon Files and Directories
4. Container Images and Build Files
5. Container Runtime
6. Docker Security Operations
7. Docker Swarm Configuration
Each control has an ID (like "5.4"), title, description, and remediation steps. Controls are marked as "scored" (required for compliance) or "not scored" (recommended).
### Why It Matters
The benchmark represents industry best practices developed by security practitioners. Following it prevents common misconfigurations that lead to breaches.
Many compliance frameworks (PCI DSS, HIPAA, SOC 2) require following CIS benchmarks or equivalent standards. This project automates checking compliance.
### How It Works
The project registers all CIS controls in `internal/benchmark/controls.go:61-end`. Each control includes metadata:
```go
Register(Control{
ID: "5.4",
Section: "Container Runtime",
Title: "Ensure that privileged containers are not used",
Description: "Using --privileged gives all capabilities to the container...",
Remediation: "Do not run containers with --privileged flag...",
Severity: finding.SeverityCritical,
Scored: true,
Level: 1,
References: []string{"https://docs.docker.com/engine/reference/run/"},
})
```
Analyzers create findings that reference these controls:
```go
control, _ := benchmark.Get("5.4")
f := finding.New("CIS-5.4", control.Title, finding.SeverityCritical, target).
WithDescription(control.Description).
WithRemediation(control.Remediation).
WithCISControl(control.ToCISControl())
```
The `--cis` flag filters findings by control ID. The SARIF output includes CIS control IDs as tags for integration with compliance tools.
### Testing Your Understanding
Before moving to the architecture, make sure you can answer:
1. Why is `CAP_SYS_ADMIN` worse than `CAP_NET_RAW`? What specific attack does each enable?
2. If a container shares the host PID namespace but has no special capabilities, can it still be dangerous? How?
3. Why do we need both pattern matching and entropy analysis for secret detection? Give an example secret that each would catch.
If these questions feel unclear, re-read the relevant sections. The implementation will make more sense once these fundamentals click.
## Further Reading
**Essential:**
- CIS Docker Benchmark v1.6.0 (PDF) - the complete specification this project implements
- Linux Programmer's Manual capabilities(7) - authoritative capability documentation
- Docker Security Documentation - official Docker security best practices
**Deep dives:**
- "Understanding and Hardening Linux Containers" (NCC Group whitepaper) - container internals and escape techniques
- "A Measurement Study of Docker Container Configurations on GitHub" (research paper) - data on real-world misconfigurations
- Kernel source: security/capability.c - how capability checks actually work in the kernel
**Historical context:**
- Original Docker security audit by ThoughtWorks (2014) - identified many issues this benchmark addresses
- "Dirty COW" (CVE-2016-5195) - kernel vulnerability exploitable from containers
- runc vulnerability CVE-2019-5736 - container escape via malicious image

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,339 +0,0 @@
# Docker Security Concepts
This document explains the security concepts behind the checks that docksec performs. Understanding these helps you know why certain configurations are flagged and how to fix them properly.
## What is the CIS Docker Benchmark
The Center for Internet Security (CIS) publishes security configuration guides called benchmarks. The Docker Benchmark is a 150+ page document that defines security best practices for Docker deployments.
Each control has:
- An ID (like "5.4" for privileged containers)
- A title describing what to check
- A rationale explaining why it matters
- Remediation steps to fix issues
- A severity level (scored vs unscored, Level 1 vs Level 2)
Level 1 controls are basic hardening that should apply to most environments. Level 2 controls provide stronger security but may break functionality.
docksec implements automated checks for controls that can be detected programmatically. Some controls require manual review (like "ensure the container host has been hardened").
## How Docker Isolation Works
Containers are not virtual machines. They share the host kernel. The isolation comes from Linux kernel features:
### Namespaces
Namespaces partition kernel resources so each container sees its own isolated copy:
| Namespace | What it isolates |
|-----------|------------------|
| PID | Process IDs (container sees itself as PID 1) |
| NET | Network interfaces, routing tables, ports |
| MNT | Filesystem mount points |
| UTS | Hostname and domain name |
| IPC | Inter-process communication (shared memory, semaphores) |
| USER | User and group IDs |
When you run `docker run nginx`, Docker creates new namespaces. The nginx process inside cannot see host processes (PID namespace), cannot bind to host network interfaces (NET namespace), and gets its own filesystem view (MNT namespace).
Breaking namespace isolation is a container escape. This is why sharing host namespaces is dangerous:
```bash
# Shares host PID namespace - container can see and signal all host processes
docker run --pid=host nginx
# Shares host network - container has full network access, can bind any port
docker run --network=host nginx
```
### Control Groups (cgroups)
Cgroups limit and account for resource usage:
```bash
# Limit memory to 512MB
docker run --memory=512m nginx
# Limit to 0.5 CPU cores
docker run --cpus=0.5 nginx
# Limit to 100 processes
docker run --pids-limit=100 nginx
```
Without limits, a container can consume all available resources (memory, CPU, disk I/O, PIDs) and crash the host or starve other containers. This is denial of service.
A fork bomb without PID limits:
```bash
# Inside unlimited container
:(){ :|:& };: # Creates processes until system dies
```
With `--pids-limit=100`, it hits the limit and stops.
## Linux Capabilities
Root traditionally had all privileges. This is too coarse. Linux capabilities break root privileges into smaller units that can be granted independently.
Docker drops most capabilities by default. A container running as root inside still cannot:
- Load kernel modules (CAP_SYS_MODULE)
- Access raw network sockets for sniffing (CAP_NET_RAW)
- Mount filesystems (requires CAP_SYS_ADMIN)
When you add capabilities back, you expand what the container can do:
```bash
# Add ability to change any file ownership
docker run --cap-add=CAP_CHOWN nginx
# Add network administration (modify routing, firewall, sniff traffic)
docker run --cap-add=CAP_NET_ADMIN nginx
```
Some capabilities are critical. Adding these is almost as bad as running privileged:
| Capability | What it allows |
|------------|----------------|
| CAP_SYS_ADMIN | Mount filesystems, namespace operations, many admin tasks |
| CAP_SYS_PTRACE | Debug any process, read memory, inject code |
| CAP_SYS_MODULE | Load kernel modules (instant root on host) |
| CAP_NET_ADMIN | Full network control, MITM attacks |
| CAP_DAC_OVERRIDE | Bypass all file permission checks |
docksec flags any container with these capabilities because they significantly weaken isolation.
### The Privileged Flag
`--privileged` gives all capabilities plus:
- Access to all host devices
- Disables seccomp and AppArmor
- Removes cgroup restrictions
```bash
docker run --privileged nginx
```
This is essentially running on the host with root access. Common scenarios where people use it:
- Running Docker inside Docker (DinD)
- Accessing hardware devices
- Debugging kernel issues
Most of these have safer alternatives. DinD can use `--privileged` on inner containers only. Device access can use `--device` to expose specific devices. Debugging should happen on test systems.
## Security Profiles: seccomp and AppArmor
### seccomp
Seccomp filters system calls. The Linux kernel has around 400 syscalls. Most programs only need a few dozen. Seccomp lets you block the rest.
Docker's default seccomp profile blocks dangerous syscalls:
- `mount` (could escape container filesystem)
- `reboot` (crash the host)
- `kexec_load` (replace running kernel)
- `bpf` (load arbitrary kernel code)
Disabling seccomp removes this protection:
```bash
docker run --security-opt seccomp=unconfined nginx
```
docksec flags `seccomp=unconfined` because it exposes the full syscall attack surface.
### AppArmor
AppArmor is a Mandatory Access Control (MAC) system. Unlike normal permissions (which processes can bypass if running as root), AppArmor rules apply regardless of privilege level.
Docker's default AppArmor profile restricts:
- Writing to certain paths (`/proc`, `/sys`)
- Mounting filesystems
- Accessing raw network
Not having an AppArmor profile means relying only on discretionary controls, which privileged processes can bypass.
## Dangerous Mount Points
Mounting host paths into containers can break isolation:
### The Docker Socket
```bash
docker run -v /var/run/docker.sock:/var/run/docker.sock nginx
```
The Docker socket gives full control over the Docker daemon. From inside the container:
```bash
# Start a privileged container that mounts the host root
docker run -v /:/host --privileged alpine chroot /host
```
Game over. You have host root.
docksec flags Docker socket mounts as CRITICAL because they provide trivial container escape.
### Sensitive Host Paths
Some paths are always dangerous to mount:
| Path | Risk |
|------|------|
| `/` | Full host filesystem access |
| `/etc` | Modify passwd, shadow, sudoers, cron |
| `/var/run` | Access to sockets including Docker |
| `/proc` | Kernel and process information, some writable |
| `/sys` | Kernel configuration, some writable |
| `/dev` | Device access |
| `/boot` | Bootloader, kernel images |
Even read-only mounts of `/etc` expose sensitive data (password hashes, private keys).
## no-new-privileges
A process can gain privileges through:
- setuid binaries (`sudo`, `passwd`)
- setgid binaries
- File capabilities
The `no-new-privileges` flag prevents this:
```bash
docker run --security-opt=no-new-privileges nginx
```
Even if an attacker compromises a container and finds a setuid binary, they cannot use it to escalate. This is defense in depth.
## Image Security
### Running as Root
By default, containers run as root (UID 0). This is root inside the container namespace. Without user namespace remapping, it maps to UID 0 on the host.
If an attacker escapes the container, they are root on the host.
Best practice is to create a non-root user:
```dockerfile
RUN useradd -r -u 1000 appuser
USER appuser
```
docksec checks images for USER instructions and flags those running as root.
### Secrets in Images
Docker images are layers. Every instruction creates a layer. Layers are immutable and distributed.
```dockerfile
ENV API_KEY=sk-secret-key-here
```
This secret is baked into the image. Anyone who pulls the image can extract it:
```bash
docker history --no-trunc myimage
```
Even if you delete a secret in a later layer, the earlier layer still contains it:
```dockerfile
COPY secrets.json /app/
RUN rm /app/secrets.json # Still in previous layer!
```
docksec checks for:
- Secrets in ENV instructions
- Secrets in ARG instructions
- Known secret patterns in build commands
Use BuildKit secrets or runtime injection instead:
```dockerfile
# BuildKit secret mount - not stored in image
RUN --mount=type=secret,id=api_key cat /run/secrets/api_key
```
### ADD vs COPY
```dockerfile
ADD https://example.com/script.sh /app/
ADD archive.tar.gz /app/
```
ADD has implicit behaviors:
- Fetches URLs (could be compromised)
- Auto-extracts archives (zip bombs, symlink attacks)
COPY just copies files. What you see is what you get.
```dockerfile
COPY script.sh /app/
COPY archive.tar.gz /app/ # Copied as-is, not extracted
```
docksec flags ADD usage because COPY is safer and more predictable.
## Network Security
### Inter-Container Communication
By default, containers on the same bridge network can communicate freely. Container A can connect to any port on Container B.
This matters when you run multiple applications. A compromised web container could attack your database container.
The `--icc=false` daemon flag disables this. Containers can only communicate through explicit links or published ports.
### Host Network Mode
```bash
docker run --network=host nginx
```
The container shares the host's network namespace. It can:
- Bind to any port
- See all network interfaces
- Sniff traffic (with CAP_NET_RAW)
This breaks network isolation entirely. Normally used for performance sensitive applications or network tools.
## Compose File Considerations
Compose files can specify all these dangerous options:
```yaml
services:
app:
privileged: true # Full host access
cap_add:
- SYS_ADMIN # Mount filesystems, etc
network_mode: host # No network isolation
volumes:
- /:/host # Full filesystem
- /var/run/docker.sock:/var/run/docker.sock # Docker control
```
docksec parses compose files and flags the same issues it finds in running containers. This catches problems before deployment.
## The Defense in Depth Model
No single control prevents all attacks. The goal is layered security:
1. **Namespace isolation** - Container cannot see host resources
2. **Capability restrictions** - Container cannot perform privileged operations
3. **seccomp filtering** - Container cannot make dangerous syscalls
4. **AppArmor/SELinux** - Mandatory access control as backup
5. **Resource limits** - Container cannot exhaust host resources
6. **Non-root user** - Compromise gives limited privileges
7. **Read-only filesystem** - Attacker cannot persist changes
8. **No privileged flag** - None of the above is bypassed
docksec checks all these layers. A single CRITICAL finding (like privileged mode) can undermine everything else.
## Further Reading
- [CIS Docker Benchmark v1.6.0](https://www.cisecurity.org/benchmark/docker)
- [Docker Security Documentation](https://docs.docker.com/engine/security/)
- [Linux Capabilities Manual](https://man7.org/linux/man-pages/man7/capabilities.7.html)
- [seccomp Documentation](https://docs.docker.com/engine/security/seccomp/)
- [AppArmor Documentation](https://docs.docker.com/engine/security/apparmor/)