Skip to content

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:

  1. Which modules touch something sensitive? — Classify by imports into four classes
  2. Can untrusted input reach dangerous code? — Find paths from ingress to execution/deserialization
  3. What's the blast radius of sensitive modules? — If a sensitive module is compromised, what else can it affect?
  4. 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:

blast radius of secrets modules
42 of 50 modules (84%) can reach the 2 module(s) handling secrets

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

  1. Classify modules by matching their imports against SENSITIVITY markers
  2. Each classification carries the exact import that triggered it
  3. Test modules are skipped unless include_test_modules=True

  4. Find dangerous paths using BFS from ingress/deserialization modules to execution/deserialization targets

  5. Shows the shortest actual import path
  6. Limited to prevent explosive search (default 6 hops)

  7. Measure blast radius by reverse BFS from secrets/execution modules

  8. "Who can reach this module?"
  9. Counts how much of the codebase has a path to sensitive code

  10. Count external surface by collecting third-party packages transitively

  11. Follows both forward (reachable_from) and backward (reaches) edges
  12. Estimates supply-chain attack surface

Key Concepts

Load-Time vs Deferred Imports

  • Load-time import: Module-level import statement; 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 subprocess to 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