Architectural Security Analyzer¶
Structural security analysis over the dependency graph — not vulnerability scanning.
What It Is NOT¶
This analyzer is not a replacement for vulnerability scanners.
- Not CVE matching: Snyk, Semgrep, and GitHub Advanced Security do that better and with far more signal. Don't abandon them.
- Not data flow analysis: This tool sees imports, not untrusted input propagation.
- Not a compliance tool: It doesn't check OWASP, CWE, or regulatory frameworks.
What it does instead: answers the structural questions that need a resolved dependency graph, and does so with complete honesty about its limits.
The Core Insight¶
Every module is classified by what it actually imports, never by what it is named.
A file called auth.py that imports nothing sensitive is not sensitive. A file called helpers.py that imports subprocess and pickle is. Naming is a guess; only imports can be checked.
What It Does¶
Answers four structural security questions:
- Which modules touch something sensitive? — Classify by imports into four classes
- Can untrusted input reach dangerous code? — Find paths from ingress to execution/deserialization
- What's the blast radius of sensitive modules? — If a sensitive module is compromised, what else can it affect?
- How much third-party surface does sensitive code drag in? — Count transitive external dependencies
The Four Sensitivity Classes¶
ingress¶
Modules that accept input from outside the process. Markers:
- HTTP frameworks: flask, fastapi, django, aiohttp, bottle, sanic
- Network: socket, socketserver, http.server
- Web middleware: starlette, wsgiref
- Rust/Go equivalents: actix_web, axum, net/http, mux
execution¶
Modules that can spawn processes or execute arbitrary code. Markers:
- subprocess, multiprocessing, ctypes, runpy
- importlib (dynamic imports as code execution)
- OS-level: os.exec, syscall, libc
- Node.js: child_process, vm
- Rust: std::process
deserialization¶
Turns bytes back into objects — historically the richest RCE surface. Markers:
- Python: pickle, marshal, shelve, dill, cloudpickle, yaml, jsonpickle
- Go: encoding/gob, gopkg.in/yaml.v2/v3
- Rust: serde_pickle, bincode
- Node.js: node-serialize
secrets¶
Handles cryptographic keys, tokens, credentials. Markers:
- Cryptography: cryptography, nacl, ring, rustls, openssl, bcryptjs
- Tokens/Auth: jwt, jsonwebtoken, keyring
- Password hashing: bcrypt, passlib, argon2
- TLS: ssl
- Go crypto: golang.org/x/crypto
hashing¶
Separate from secrets because it's often checksums, not cryptography. This project's own diagram module hashes file paths to make node IDs — calling that "handles secrets" would destroy the credibility of every other finding. Markers:
- hashlib, hmac, (Python standard library hashing)
- Language equivalents for MD5, SHA1 (marked as weak but distinguished)
Test modules (*_test.py, *_test.go, etc.) are excluded by default — they are not deployed, so a test importing subprocess is not an attack surface.
The Three Finding Types¶
1. Dangerous Paths¶
High severity paths from ingress modules to execution or deserialization modules. Example:
ingress can reach execution
3 module(s) classified `ingress` have an import path to a module classified `execution`
Why it matters: User input → code execution is classic RCE.
Shows: - The path itself (every step must be a real load-time import edge) - Number of source and target modules - Sample paths (limited to keep findings readable)
2. Blast Radius¶
Medium/Low severity depending on scale. Example:
Why it matters: If a secrets module is compromised or changed, how much code could be affected?
Shows: - Module count and percentage - Sample modules in the blast radius - Severity downgraded if the radius is small (< 50% of codebase)
3. Supply-Chain Surface¶
Medium/Info severity depending on scale. Example:
third-party surface reachable from ingress code
code classified `ingress` transitively depends on 47 third-party package(s)
Why it matters: The more external code you load, the larger your supply-chain attack surface.
Shows:
- Package count
- Sample packages (first 40)
- Severity upgraded to medium if over 30 packages
The Crucial Limitation¶
This tool sees imports, not data flow.
A path from an ingress module to a subprocess call means that path exists in the dependency graph. It does not mean user input demonstrably reaches that call. Code between them may sanitize, validate, or reject the input.
Example: A Flask handler that calls subprocess is flagged. But if the handler is authenticated_admin_only and sanitizes its input, the actual risk is much lower than the path implies.
Use these findings as places to look, not as proof of exploitability. A security report that overstates its certainty is worse than no report, because it spends attention that had somewhere better to go.
Usage¶
CLI¶
# JSON output (default)
drydock security <project_path>
# With options
drydock security <project_path> \
--max-paths 10 \
--path-limit 8 \
--include-hashing \
--include-test-modules
# Markdown (human-readable)
drydock security <project_path> --markdown
MCP¶
drydock_security(project_path: str, max_paths: int = 5,
path_limit: int = 6, include_hashing: bool = False,
include_test_modules: bool = False) -> str
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
max_paths |
int | 5 | How many example paths to show per finding |
path_limit |
int | 6 | Longest import chain to search when finding a path |
include_hashing |
bool | False | Treat hashing (often checksums) as sensitive too |
include_test_modules |
bool | False | Classify test modules too; by default they are excluded |
How It Works¶
- Classify modules by matching their imports against
SENSITIVITYmarkers - Each classification carries the exact import that triggered it
-
Test modules are skipped unless
include_test_modules=True -
Find dangerous paths using BFS from ingress/deserialization modules to execution/deserialization targets
- Shows the shortest actual import path
-
Limited to prevent explosive search (default 6 hops)
-
Measure blast radius by reverse BFS from secrets/execution modules
- "Who can reach this module?"
-
Counts how much of the codebase has a path to sensitive code
-
Count external surface by collecting third-party packages transitively
- Follows both forward (reachable_from) and backward (reaches) edges
- Estimates supply-chain attack surface
Key Concepts¶
Load-Time vs Deferred Imports¶
- Load-time import: Module-level
importstatement; creates true circular dependency - Deferred import: Inside a function or
if TYPE_CHECKING:block; creates no load-time dependency
This analyzer only traces load-time imports. Deferred imports are how registries deliberately invert their dependencies (import their own plugins inside a function), and counting them as cycles would be misleading.
Classification by Import, Not Name¶
Why? Because names lie. A security.py file with no sensitive imports is not sensitive. A util.py file importing pickle is.
Evidence. Every classification includes the exact import specifier that triggered it, so a reader can verify or dispute the claim.
Example Output¶
JSON¶
{
"meta": {
"project": "myapp",
"root": "/path/to/myapp",
"files_walked": 47,
"truncated": false
},
"summary": {
"modules": 47,
"classified": 8,
"tests_included": false,
"by_class": {
"ingress": 3,
"execution": 2,
"deserialization": 1,
"secrets": 2
},
"findings": 2,
"by_severity": {
"high": 1,
"medium": 1,
"low": 0,
"info": 0
}
},
"classified": [
{
"module": "handlers/auth.py",
"classes": ["secrets"],
"evidence": [
{"class": "secrets", "import": "cryptography"}
]
}
],
"findings": [
{
"id": "path-ingress-to-execution",
"title": "ingress can reach execution",
"severity": "high",
"summary": "1 module(s) classified `ingress` have an import path to a module classified `execution`.",
"paths": [
{
"from": "routes/upload.py",
"to": "utils/archive.py",
"hops": 2,
"path": ["routes/upload.py", "handlers/unzip.py", "utils/archive.py"]
}
]
}
],
"limitation": "Imports are analysed, not data flow. A reported path exists in the dependency graph; it is not evidence that untrusted input reaches the call. Use these as places to look."
}
Markdown¶
# Architectural security: myapp
8 of 47 modules touch something security-relevant · 2 findings
> Imports are analysed, not data flow. A reported path exists in the dependency graph...
## Modules by class
| Class | Modules |
|---|---|
| `ingress` | 3 |
| `execution` | 2 |
| `deserialization` | 1 |
| `secrets` | 2 |
## Findings
### ingress can reach execution — high
1 module(s) classified `ingress` have an import path to a module classified `execution` -- input from outside the process can reach code execution.
- `routes/upload.py` → `handlers/unzip.py` → `utils/archive.py`
### blast radius of secrets modules — medium
4 of 47 modules (8.5%) can reach the 2 module(s) handling secrets.
<details><summary>evidence</summary>
- `secrets_modules`: handlers/auth.py, handlers/keys.py
- `reachable_from_count`: 4
- `share_of_codebase`: 0.085
- `sample`: api/__init__.py, handlers/token.py, ...
</details>
When Findings Are Most Useful¶
- New service accepting untrusted input: Run security on day 1 to catch unintended code execution paths
- Third-party dependency update: Re-run to see if blast radius changed
- Refactoring sensitive paths: Run before and after to verify isolation improved
- Code review prep: Point reviewers to high-severity findings as audit targets
- Threat modeling: "Our auth module is 50% of the codebase blast radius — should it be more isolated?"
When Findings May Be Noisy¶
- Harmless CLI tools: A CLI that calls
subprocessto invoke external tools is normal and often safe - Frameworks with many dependencies: High third-party surface may just mean "we use a real framework"
- Validated input: A path from ingress to execution doesn't mean the input is unvalidated
- Deferred loading: Plugins loaded at runtime (inside functions) won't appear in load-time paths
Supported Languages¶
All languages supported by Drydock's import analysis: - Python - Go - Rust - TypeScript/JavaScript - Java - C#
Import detection uses language-native syntax parsing, so the analyzer works correctly across ecosystems.
Complementary Tools¶
This analyzer works best alongside:
- Vulnerability scanners (Snyk, Semgrep, GHAS) — catch known CVEs
- SAST tools — find data flow and control flow issues
- Network/runtime monitors — catch actual exploitation attempts
- Fuzz testing — find unvalidated inputs in practice
Use this tool to answer: "Structurally, where could untrusted input go?" Use scanners to answer: "Are there known bugs on that path?"
Further Reading¶
- OWASP CWE-94 — Improper Control of Generation of Code
- OWASP Deserialization — Why deserialization is dangerous
- Threat Modeling Book — Adam Shostack on asking the right security questions