
Threat Model
- 177 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
threat-model: A skill for development. This provides functionality for development workflows.
Key points
- threat-model
Threat Model by the numbers
- 177 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,232 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill threat-modelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 177 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use threat-model for development tasks?
Use threat-model for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with threat-model.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use threat-model for development tasks, or when threat-model: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to threat-model: threat-model.
Files
Threat Model
Produces structured, evidence-backed security threat models for any codebase. Goes beyond surface enumeration by tracing untrusted data through actual code paths, clustering findings by root cause, and constructing exploit chains that combine individual findings into higher-severity attack paths.
When to Apply
- User asks to threat model, security review, or map attack surfaces for a codebase
- Starting work on security-sensitive features (auth, crypto, file I/O, networking, native bridges)
- Evaluating a new codebase or major architectural change for security implications
- Reviewing a PR or recent commits for security regressions (incremental/diff mode)
- After a security incident to reassess the threat landscape
Workflow Overview
Phase 0 (conditional): Diff Analysis — if git range provided, scope to changed code
Phase 1: Codebase Survey → Understand what the project is and does
Phase 2: Component Mapping → Identify components, data flows, and language bridges
Phase 3: Asset Identification → Determine what needs protecting
Phase 4: Trust Boundaries → Classify inputs by trust level, inventory entry points
Phase 5: Data Flow Tracing → Follow untrusted values from entry to sink ← key technique
Phase 6: Attack Surface Enum → Document surfaces with traced evidence
Phase 7: Pattern Clustering → Group 3+ similar findings by root cause
Phase 8: Exploit Chains → Combine findings into multi-step attack paths
Phase 9: Calibration → Rate with chain-adjusted and systemic severity
Phase 10: Output → Write structured THREAT-MODEL.mdHow to Use
1. Read methodology for the detailed approach at each phase 2. Read output format for the document structure (6 sections) 3. Consult attack patterns for technology-specific patterns 4. Run scripts/trace-data-flows.sh <project-root> to inventory entry points and sinks 5. Optionally run scripts/scan-patterns.sh <project-root> for security-relevant code patterns
Analytical Techniques
These techniques are the skill's core value — they encode analytical methods that produce findings the model wouldn't generate from general knowledge alone.
| Technique | When to Read | What It Adds |
|---|---|---|
| Data Flow Tracing | Phase 5 — always | Traces untrusted input from entry to sink through actual code. Produces evidence-backed findings instead of theoretical risks |
| Pattern Clustering | Phase 7 — after enumeration | Groups related findings by root cause. Recommends systemic fixes instead of individual patches |
| Exploit Chains | Phase 8 — after clustering | Combines findings into multi-step attack paths rated by terminal impact |
| Bridge Analysis | Phase 6 — when FFI/bridges found | Systematic checklist for cross-language boundaries (Swift↔C, Rust↔C, Rails↔NGINX) |
| Diff Analysis | Phase 0 — for incremental review | Scopes analysis to changed code, identifies regressions |
Key Principles
- Evidence over speculation: Every finding should include a data flow trace showing how untrusted input reaches the vulnerable operation. "XSS is possible" is speculation. "RFC markdown → marked.parse() → innerHTML at line 917 with no sanitizer" is evidence.
- Systemic over individual: When 3+ findings share a root cause, the systemic finding is more important than any individual finding. Fix the root cause, not the symptoms.
- Chains over singletons: Rate combined attack paths by their terminal impact. Three medium findings that chain into critical impact are a critical finding.
- Existing mitigations matter: Document what's already protected, not just what's missing.
- Context-aware calibration: Severity depends on deployment context. Always include scope notes.
Output
Produces two files (configurable via config.json):
- `findings.json` — Structured, machine-readable findings. Source of truth. Consumed by
threat-patchfor automated remediation. Tracks finding state across runs (open → patched → verified → closed). - `THREAT-MODEL.md` — Human-readable view generated from findings.json. 6 sections: Overview, Trust Boundaries, Attack Surfaces, Systemic Findings, Exploit Chains, Criticality Calibration.
Pipeline Integration
threat-model → findings.json → threat-patch (consumes findings, generates fixes)
↑ ↓
└── threat-model --diff (re-analyzes, updates finding status) ←── git commitsWhen findings.json exists from a prior run, the skill reads it to:
- Track which findings are still open vs patched
- Calibrate severity against prior ratings
- Detect regressions (fixed findings that reappeared)
Two Modes
| Mode | Trigger | What It Does |
|---|---|---|
| Full analysis | "threat model this codebase" | Analyzes entire codebase, produces fresh findings.json + THREAT-MODEL.md |
| Diff analysis | "what changed since last review" / git range provided | Scopes to changed code, updates existing findings.json with new/resolved/regressed findings |
Diff mode is the daily driver for ongoing projects. Full mode runs once (or periodically).
References
| File | When to Read |
|---|---|
| references/methodology.md | Before starting — the 10-phase workflow |
| references/output-format.md | When writing output — 6-section template |
| references/findings-schema.md | When writing findings.json — structured schema |
| references/attack-patterns.md | When enumerating surfaces — technology patterns |
| references/techniques/ | During specific phases — analytical techniques |
{
"output_path": "THREAT-MODEL.md",
"diff_range": "",
"_setup_instructions": {
"output_path": "Where to write the threat model relative to the project root (default: THREAT-MODEL.md)",
"diff_range": "Git range for incremental analysis (e.g., HEAD~10..HEAD or main...feature-branch). Leave empty for full analysis"
}
}
Gotchas
Data flow tracing can exhaust context on large codebases
Following every entry point through every function call generates a lot of reads. On large codebases (1000+ source files), prioritize: trace attacker-controlled inputs first, skip developer-controlled inputs, and focus on modules where the trace-data-flows.sh script identifies overlapping entry points and sinks. Added: 2026-03-28
Over-chaining produces implausible attack paths
Limit chains to 4 steps maximum. Each step must be independently exploitable. If a chain requires 5+ steps or unrealistic preconditions (physical access, root, timing window < 1ms), it's theoretical, not practical. Rate-limit your chain construction to avoid diluting real findings with noise. Added: 2026-03-28
Pattern clustering threshold: 3+ instances, not 2
Two findings of the same class is coincidence, not a systemic pattern. Only cluster when you find 3+ instances AND they share the same root cause (missing abstraction, helper, or policy). Two XSS findings from different causes (one missing escaping, one missing CSP) are not a cluster. Added: 2026-03-28
The scan and trace scripts require ripgrep (rg)
Both scan-patterns.sh and trace-data-flows.sh depend on rg (ripgrep) for pattern matching. If rg is not installed, the scripts will exit with an error. Install via brew install ripgrep (macOS) or apt install ripgrep (Debian/Ubuntu). Added: 2026-03-28
Large codebases may exceed context limits during analysis
If the codebase is too large to read entirely, focus data flow tracing on files where the trace script identifies both entry points and sinks. Skip vendored/generated code. For incremental analysis, use diff mode (Phase 0) to scope to recent changes. Added: 2026-03-28
Bridge analysis only applies when cross-language boundaries exist
Don't force bridge analysis on single-language projects. The technique is high-value for Swift↔C, Rust↔C, Ruby↔C, Node.js↔C++ codebases but irrelevant for pure-language projects. Added: 2026-03-28
{
"version": "1.0.3",
"organization": "pproenca",
"technology": "Security Threat Modeling",
"discipline": "composition",
"type": "automation",
"date": "March 2026",
"abstract": "Produces structured, evidence-backed security threat models for any codebase. Goes beyond surface enumeration with five analytical techniques: data flow tracing (entry-to-sink), pattern clustering (root cause grouping), exploit chain construction (combining findings), cross-language bridge analysis, and incremental diff analysis. Designed to match the finding depth and severity accuracy of automated security scanning tools.",
"references": [
"https://owasp.org/www-community/Threat_Modeling",
"https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool",
"https://cheatsheetseries.owasp.org/cheatsheets/Threat_Modeling_Cheat_Sheet.html"
]
}
Attack Patterns by Technology Domain
Reference guide for common attack surface patterns. Use this as a starting point when enumerating surfaces — not as a checklist. The actual surfaces in the threat model should be derived from the codebase, not copied from this list.
CLI Tools & Filesystem Operations
Path traversal
What to look for: User-supplied strings used in path construction without normalization or containment checks.
- Bundle IDs, filenames, or identifiers concatenated into paths (e.g.,
/tmp/tool-name/\(userInput)) ../sequences not stripped from filenames read from manifests, configs, or user input- Output directories that accept arbitrary paths (
--output ../../etc/)
Grep patterns: \(bundleID\), \(filename\), appendingPathComponent, join(path, /tmp/ + variable
Predictable temporary files
What to look for: Fixed paths under /tmp or NSTemporaryDirectory() without unique naming or exclusive creation.
- Hardcoded paths like
/tmp/tool-output.jsonor/tmp/tool-name/ removeItemfollowed bycreateDirectoryat the same path (race window)- No
O_EXCL,mkstemp,mkdtemp, or UUID-based naming
Grep patterns: /tmp/, NSTemporaryDirectory, removeItem.*createDirectory, FileManager.default.createDirectory
Symlink attacks
What to look for: File operations at predictable paths without symlink verification.
- Writing to paths that could be symlinks without checking
resourceValues(forKeys: [.isSymbolicLinkKey]) - Deleting and recreating directories in world-writable locations
- Copying files from untrusted locations without verifying link status
Destructive operations
What to look for: CLI subcommands that delete, erase, or overwrite without confirmation.
FileManager.removeItemon user-supplied paths- Device erase/reset commands callable without confirmation
- Uninstall operations that don't verify ownership
Web & HTML Generation
XSS in generated HTML
What to look for: Untrusted data embedded in HTML without context-appropriate escaping.
- JSON serialized into
<script>tags —</script>sequences in values can break out - String interpolation into
innerHTML,outerHTML, or HTML attributes - Escaping that covers
<>&but not quotes (attribute breakout) - Markdown renderers with raw HTML enabled (e.g.,
marked.parse()without sanitizer)
Grep patterns: innerHTML, outerHTML, const R=, <script>, marked.parse, dangerouslySetInnerHTML
SSRF
What to look for: URLs constructed from user/operator input used in server-side HTTP requests.
- Config values like
NGINX_URLorSERVICE_URLused inNet::HTTP,fetch(), orcurl - Diagnostics/health-check endpoints that proxy to internal URLs
- Redirect URLs from user input
Missing authentication
What to look for: CRUD endpoints or mutation operations without auth middleware.
- Rails controllers without
before_action :authenticate - Express routes without auth middleware
- API endpoints that rely on network segmentation instead of auth
config.hosts.clearor similar host validation disabling
Default credentials
What to look for: Hardcoded passwords, API keys, or secret values in config or setup scripts.
- Docker compose files with fixed
SECRET_KEY_BASEor API keys - Setup scripts that create admin users with known passwords
- Demo/seed scripts that expose credential patterns
Native Code & Memory Safety
Buffer/bounds issues
What to look for: Unsafe pointer operations, unchecked sizes, or missing bounds validation in C/C++/ObjC.
load(fromByteOffset:)orwithUnsafeByteswithout bounds checking against data size- Loop counters from untrusted data (e.g., Mach-O
ncmds) without validation String(cString:)on potentially unterminated buffersreallocor allocation based on untrusted size values without upper bounds
Grep patterns: withUnsafeBytes, load(fromByteOffset, String(cString, realloc, malloc
Use-after-free / lifetime issues
What to look for: Resources used after their owning scope has ended, especially across async boundaries.
- Callbacks or closures that capture pointers to stack-allocated or session-scoped objects
- Timeout paths that return early while background work continues using freed resources
dispatch_asyncorTask.detachedusing a session/context that the caller destroys on return
dlopen / dynamic loading
What to look for: Framework or library loading from paths derived from environment or tools.
dlopenwith paths built fromxcode-select -por similar — empty/relative output creates hijack opportunity- Plugin loading from user-writable directories
- No validation that loaded library is signed or from expected location
Injection Attacks
Command/expression injection
What to look for: Untrusted input interpolated into commands, queries, or expressions.
- String interpolation into shell commands, LLDB expressions, SQL queries
- User-supplied values in
Process.argumentsorNSTaskwithout escaping - Template strings that embed user data into executable contexts
Grep patterns: Process(, NSTask, system(, popen(, exec(, eval(, expression --
SQL injection
What to look for: String concatenation in SQL queries (less common with ORMs but check raw queries).
- Direct string interpolation in SQLite or ActiveRecord
whereclauses - Prepared statements that build the SQL string before preparing
Mobile & iOS
Keychain / credential persistence
What to look for: Credential lifecycle issues around sign-in, sign-out, and device sharing.
- Cached profile data not cleared on sign-out (PII leakage to next user)
- Token age/expiry checks that fail open (allowing continued access after revocation)
- Biometric locks that unlock when biometrics unavailable (fail-open)
Data persistence across sessions
What to look for: Local storage (SwiftData, CoreData, UserDefaults) not scoped to authenticated user.
- Cached data visible to the next user after sign-out
- Background refresh that continues after auth revocation
- Local stores that don't validate current session before returning data
App attestation / integrity
What to look for: Removal or weakening of client integrity checks.
- App Attestation or SafetyNet integration removed during refactors
- Attestation made optional without compensating server-side enforcement
Dependencies & Supply Chain
Unpinned dependencies
What to look for: Dependencies on branches instead of tags/commits.
branch: "main"orbranch: "master"in Package.swift, Gemfile, package.jsoncurl | bashinstallation without checksum verification- Git submodules at HEAD without pinned commits
Asset integrity
What to look for: Packaged scripts, templates, or config files that execute with tool privileges.
- LLDB scripts, build scripts, or migration files in user-writable locations
- Assets loaded at runtime without signature or checksum verification
- Config files in untrusted project directories (
.tool-name/config.jsonin a cloned repo)
Data Serialization & Parsing
Unbounded parsing
What to look for: JSON/XML/protobuf parsing without size limits on untrusted input.
JSONSerializationorcJSON_Parseon payloads without checkingContent-Lengthor buffer size- Recursive tree parsing without depth limits (stack overflow on deep trees)
- Decompression without output size bounds (zip bombs, gzip ISIZE manipulation)
Grep patterns: JSONSerialization, cJSON_Parse, JSONDecoder, Decompression, gunzip
Non-finite numeric values
What to look for: Float/Double values from untrusted sources used without NaN/Inf checks.
Double→Intconversions that trap on non-finite values (Swift fatal error)- NaN propagation through calculations that produce invalid JSON or corrupt state
- CSS/coordinate values from untrusted UI data without range validation
Information Disclosure
Sensitive data in logs/output
What to look for: Context identifiers, tokens, or PII in logs, WAL files, or status endpoints.
- Access logs including
context_key, user IDs, or session tokens - WAL files or event logs in world-readable directories
- Status/diagnostics endpoints returning internal telemetry without auth
- Environment variable overrides logged to disk (may contain secrets)
findings.json Schema
The structured output format that connects the threat-model → threat-patch pipeline. This is the source of truth; THREAT-MODEL.md is a human-readable view generated from it.
Why Machine-Readable Output
A Markdown threat model gets read once and filed. A structured findings file:
- Feeds directly into
threat-patchfor automated remediation - Tracks state across runs (open → patched → verified → closed)
- Enables incremental analysis (diff mode updates the same file)
- Calibrates future runs against historical severity ratings
Schema (v1)
{
"schema_version": "1.0",
"metadata": {
"project": "project-name",
"analyzed_at": "2026-03-28T12:00:00Z",
"git_ref": "abc123def456",
"scope": "full | diff:HEAD~10..HEAD",
"tool_version": "threat-model 0.2.0"
},
"findings": [
{
"id": "TM-001",
"title": "Short descriptive title",
"severity": "critical | high | medium | low",
"category": "CATEGORY_TAG",
"status": "open | patched | verified | closed | wont_fix",
"description": "1-3 sentence explanation of the vulnerability",
"trace": {
"entry": {
"file": "path/to/file.swift",
"line": 19,
"type": "cli_arg | http_param | file_content | env_var | ipc | deserialization",
"variable": "pid"
},
"steps": [
{
"file": "path/to/file.swift",
"line": 37,
"function": "functionName",
"operation": "pass-through | transform | partial-validation",
"detail": "What happens at this step"
}
],
"sink": {
"file": "path/to/file.swift",
"line": 42,
"operation": "file_write | exec | allocation | html_render | sql_query | network_request",
"impact": "What the attacker achieves"
}
},
"mitigations": ["Existing control 1", "Existing control 2"],
"attacker_story": "Concrete exploitation scenario",
"recommended_fix": "What to do about it",
"relevant_paths": ["path/to/affected/file1.swift", "path/to/affected/file2.swift"],
"systemic_parent": "SYS-001 or null",
"chain_memberships": ["CHAIN-001"],
"detected_at": "2026-03-28T12:00:00Z",
"resolved_at": null,
"resolved_by": null
}
],
"systemic": [
{
"id": "SYS-001",
"title": "Root cause description",
"severity": "high",
"category": "CATEGORY_TAG",
"instance_count": 8,
"root_cause": "What abstraction, policy, or helper is missing",
"recommended_fix": "Single change that resolves all instances",
"finding_ids": ["TM-001", "TM-003", "TM-005"],
"affected_files": ["file1.swift", "file2.swift"]
}
],
"chains": [
{
"id": "CHAIN-001",
"title": "Descriptive chain name",
"severity": "critical",
"steps": [
{
"finding_id": "TM-001",
"provides": "What this step gives the attacker",
"requires": "What preconditions this step needs"
},
{
"finding_id": "TM-003",
"provides": "What the chain ultimately achieves",
"requires": "What it uses from the previous step"
}
],
"terminal_impact": "The final outcome of the full chain",
"preconditions": "What must be true for the chain to work",
"chain_breaking_fix": "Which single finding to fix to break this chain"
}
]
}Field Reference
Finding Categories
Use consistent tags for clustering:
| Category | Pattern |
|---|---|
PATH_TRAVERSAL | Unsanitized input in path construction |
PREDICTABLE_TMP | Fixed paths under /tmp without unique naming |
SYMLINK_RACE | File ops at predictable paths without link checks |
XSS_NO_SANITIZE | Untrusted data in HTML without escaping |
INJECTION | Input interpolated into commands/queries/expressions |
UNBOUNDED_ALLOC | Allocation sized by untrusted value without cap |
LIFETIME_RACE | Resource used after owning scope ends |
MISSING_AUTH | Mutation endpoint without authentication |
INFO_DISCLOSURE | Internal data exposed without access control |
PROMPT_INJECTION | Untrusted content injected into LLM context |
SUPPLY_CHAIN | Unpinned dependency or unverified asset |
DEFAULT_CREDS | Hardcoded passwords or API keys |
Finding Status Lifecycle
open → patched (fix applied, not verified)
→ verified (fix confirmed by re-analysis)
→ closed (verified + merged)
→ wont_fix (accepted risk, documented reason)Status transitions happen when:
threat-patchapplies a fix → status moves topatched,resolved_byset to commit hashthreat-model --diffre-analyzes and confirms the fix → status moves toverified- Manual review closes the finding → status moves to
closedorwont_fix
Trace Entry Types
| Type | When to Use |
|---|---|
cli_arg | CLI argument or flag value |
http_param | HTTP request parameter, header, cookie, body field |
file_content | Data read from a file (including config, JSON, CSV) |
env_var | Environment variable value |
ipc | Inter-process communication (Redis, sockets, notifications) |
deserialization | Parsed data (JSON, YAML, protobuf, pickle) |
Sink Operations
| Operation | Impact Class |
|---|---|
file_write | File overwrite, creation, deletion |
exec | Command execution, process spawning, LLDB expressions |
allocation | Memory allocation sized by untrusted value |
html_render | DOM injection, innerHTML, template rendering |
sql_query | Database query with untrusted input |
network_request | Outbound HTTP/network request (SSRF surface) |
llm_context | Data injected into LLM prompt/context (prompt injection) |
Compatibility with Codex CSV
The findings.json format is a superset of the Codex CSV format. The threat-patch skill can consume either:
| Codex CSV Field | findings.json Equivalent |
|---|---|
title | finding.title |
description | finding.description |
severity | finding.severity |
relevant_paths | finding.relevant_paths |
commit_hash | metadata.git_ref |
status | finding.status |
| (not in CSV) | finding.trace — data flow evidence |
| (not in CSV) | finding.systemic_parent — root cause link |
| (not in CSV) | finding.chain_memberships — exploit chain links |
| (not in CSV) | systemic[] — clustered root causes |
| (not in CSV) | chains[] — multi-step attack paths |
The additional fields are what make threat-model output richer than a scanner's CSV — they encode analytical work (traces, clusters, chains) that a scanner doesn't perform.
Threat Modeling Methodology
Follow these phases sequentially. Each builds on the previous one. Phases 5, 7, and 8 are the analytical core — they encode techniques that distinguish this analysis from a generic security review.
Phase 0: Diff Analysis (conditional)
When to use: If the user provides a git range, commit hash, or asks about "what changed" — scope the analysis to the diff instead of the full codebase.
Read techniques/diff-analysis.md and follow its workflow. Skip Phases 1-4 (use the existing threat model for context) and start at Phase 5 with only the changed code.
Phase 1: Codebase Survey
Goal: Understand what the project is, how it's deployed, and what its primary security concerns are.
Actions: 1. Read README, CLAUDE.md, architecture docs, and any existing security documentation 2. List the top-level directory structure to understand project organization 3. Identify the primary language(s), frameworks, and build system 4. Check for network endpoints, CLI entry points, config file formats, and deployment targets 5. Read dependency manifests (Package.swift, package.json, Cargo.toml, go.mod, Gemfile, requirements.txt)
Why this matters: The deployment model determines which threats are relevant. A local CLI tool has fundamentally different threats than a multi-tenant web service.
Output: A 2-3 paragraph overview describing what the project does, its key components, and its deployment model.
Phase 2: Component Mapping + Bridge Identification
Goal: Identify the distinct runtime components, how data flows between them, and where cross-language bridges exist.
Actions: 1. Map each major module/package/service to its role (UI layer, service layer, data layer, bridge code) 2. Identify data flows: what data enters the system, how it's processed, and where it exits 3. Note privilege levels — what can each component access? (filesystem, network, other processes, system APIs) 4. Identify all cross-language bridges — C/ObjC from Swift, JNI from Java, FFI from Rust, shared memory between services. For each bridge, note what types cross the boundary and who manages memory.
If bridges are found, read techniques/bridge-analysis.md and apply its systematic checklist during Phase 6.
Phase 3: Asset & Security Goal Identification
Goal: Define what needs protecting and what a successful attack looks like.
Think about:
- Host integrity: Can the tool be used to compromise the machine it runs on?
- Data confidentiality: What sensitive data does the system handle?
- Data integrity: Can an attacker corrupt stored data or outputs?
- Availability: Can the system be crashed or rendered unusable?
- Downstream trust: Do other systems trust this system's output?
Phase 4: Trust Boundaries + Entry Point Mapping
Goal: Classify every input by who controls it, and build a concrete inventory of entry points for data flow tracing.
Trust tiers (from most dangerous to least)
Attacker-controlled: Data an adversary can directly influence — CLI arguments, HTTP requests, file contents from untrusted sources, data from apps under test.
Operator-controlled: Configuration set by the deployer — config files, environment variables, deployment manifests. Can become attacker-controlled if the operator environment is compromised.
Developer-controlled: Source code, build scripts, packaged assets. Only a threat if the supply chain is compromised.
Entry point inventory
For each attacker-controlled input, record:
- Variable name holding the untrusted value
- File and line where it enters the system
- What controls it (CLI user, HTTP client, file author, upstream service)
Run scripts/trace-data-flows.sh <project-root> to automate the initial inventory. This feeds directly into Phase 5.
Phase 5: Data Flow Tracing
This is the highest-value analytical phase. Instead of listing what components exist and what could theoretically go wrong, you follow specific untrusted values through actual code and find where they reach privileged operations without validation.
Read techniques/data-flow-tracing.md for the complete technique.
The core loop: 1. Take each entry point from Phase 4 2. Grep for the variable name, follow it through function calls 3. At each step: is the value validated? transformed? passed through unchanged? 4. If it reaches a sink (file write, exec, allocation, HTML render) without validation → FINDING 5. Document the complete trace: entry → [fn: no validation] → [fn: transforms] → sink
Why this matters: This is the technique that Codex uses to find findings like --pid → unvalidated → LLDB attach to any process, or bundleID → unvalidated → path traversal in /tmp/. The model's default behavior is to identify surfaces by component; data flow tracing follows the actual code paths and produces evidence-backed findings.
Phase 6: Attack Surface Enumeration
Goal: For each significant component, document concrete attack surfaces using evidence from data flow tracing.
Each finding from Phase 5 becomes an attack surface subsection. For surfaces not discovered by tracing (e.g., configuration issues, missing auth, information disclosure), enumerate them here using the operation-to-risk mapping:
| Data Operation | Risk Class |
|---|---|
| Concatenate into path | Path traversal, symlink attacks |
| Concatenate into command/query | Injection (SQL, command, LLDB) |
| Embed in HTML/template | XSS (reflected, stored, DOM) |
| Deserialize/parse | Memory corruption, DoS, type confusion |
| Write to predictable location | Symlink race, file overwrite |
| Allocate based on untrusted size | Memory exhaustion, DoS |
| Execute as code | RCE, privilege escalation |
For each surface, document: Surface (specific files/functions), Risks (what goes wrong), Mitigations/controls (what's already there), Attacker story (concrete scenario with preconditions).
If cross-language bridges were identified in Phase 2, apply techniques/bridge-analysis.md to each bridge now.
Phase 7: Pattern Clustering
Goal: Group findings that share a root cause into systemic findings worth more attention than individual bugs.
Read techniques/pattern-clustering.md for the complete technique.
The core process: 1. Tag each finding from Phase 6 with its vulnerability class 2. Count instances per class 3. Groups with 3+ instances → identify the shared root cause (the missing abstraction, policy, or helper) 4. Rate systemic findings higher than individual findings (they fix more with one change) 5. Recommend the single fix that resolves the entire cluster
Why this matters: 8 predictable-tmp findings in agent-sim are symptoms. The root cause is "no secure temporary directory abstraction." Systemic findings guide better remediation and prevent future instances.
Phase 8: Exploit Chain Construction
Goal: Identify multi-step attack paths where individual findings combine into worse outcomes.
Read techniques/exploit-chains.md for the complete technique.
The core process: 1. For each finding, identify what access/information it PROVIDES and what it REQUIRES 2. If finding A's output satisfies finding B's input → chain A→B 3. Rate the chain by its terminal impact, not its weakest link 4. Identify chain-breaking controls — which single fix breaks the most chains
Why this matters: Path traversal (medium) + predictable tmp (medium) + symlink race (medium) = arbitrary file overwrite with attacker content (critical). Individual ratings miss the combined risk.
Phase 9: Calibration
Goal: Rate all findings — individual, systemic, and chain — by severity.
Calibration framework
| Level | Criteria |
|---|---|
| Critical | Host compromise, arbitrary code execution, complete data breach, critical exploit chains |
| High | Significant data exfiltration, major functionality bypass, widespread DoS, systemic findings with 5+ instances |
| Medium | Limited DoS, constrained data leaks, bypasses with preconditions |
| Low | Edge cases, minor reliability issues, theoretical risks |
Severity adjustments
- Chains: Rate by terminal impact. A chain of mediums reaching critical impact is critical.
- Systemic findings: Bump one level if 5+ instances AND highly centralizable fix.
- Context: Adjust for deployment model. Always include scope notes.
Out-of-scope
Include an explicit "Out-of-scope / not applicable" subsection listing threat classes that don't apply and why.
Historical calibration
If a findings.json exists from a prior run, read it before calibrating:
- Consistency: If the same finding was rated medium last time, rate it medium again unless something changed. Document the reason if you deviate.
- State tracking: Findings that were
patchedorverifiedin the prior run should be checked — are they still fixed, or did a regression reintroduce them? - Severity trends: If the prior run's severity for a category was adjusted by user feedback, inherit that calibration.
Phase 10: Output
Produce two files:
findings.json (source of truth)
Write structured findings using the schema in findings-schema.md. This file:
- Is consumed by
threat-patchfor automated remediation - Tracks finding state across runs (open → patched → verified → closed)
- Enables incremental analysis (diff mode updates the same file)
- Contains data flow traces, systemic groupings, and exploit chains as structured data
When updating an existing findings.json (diff mode or re-analysis):
- Preserve
status,resolved_at,resolved_byfor findings that haven't changed - Add new findings with
status: "open" - Mark removed findings as
status: "closed"(don't delete — preserve history)
THREAT-MODEL.md (human view)
Write the human-readable document using the format in output-format.md. This is generated FROM the findings.json data — the two files should be consistent.
Final checks:
- findings.json and THREAT-MODEL.md contain the same findings (no drift)
- Every finding has a
tracewith entry, steps, and sink - Systemic findings reference their child finding IDs
- Chains reference their constituent finding IDs
- Calibration considers deployment context and chain-adjusted severity
- Out-of-scope section is explicit
Output Format
The threat model document follows a four-section structure. Use the templates below, replacing placeholders with analysis results.
Document Structure
## 1. Overview
## 2. Threat model, Trust boundaries and assumptions
## 3. Attack surface, mitigations and attacker stories
## 4. Systemic findings (root cause clusters)
## 5. Exploit chains (multi-step attack paths)
## 6. Criticality calibration---
Section 1: Overview
A concise description of the system. Cover: what it does, key components, runtime architecture, deployment model, and primary security goals.
Template:
## 1. Overview
{project-name} is a {type: CLI/web service/mobile app/library} for {purpose}. It {key capabilities, 2-3 sentences covering architecture and major components}. {Describe the layers/modules and their roles}. Low-level {operations/access} is implemented through {specific mechanisms}. Output is {what the system produces and where}.
The tool/service runs {deployment context: locally/in cloud/on device}, {network posture: without network endpoints/with public endpoints/behind a VPN}, and is typically invoked by {who uses it}. The primary security goals are: {goal 1}, {goal 2}, and {goal 3}.Guidelines:
- Name specific source directories, modules, and frameworks
- State the deployment model explicitly (local, networked, multi-tenant)
- End with 2-3 concrete security goals that frame the rest of the analysis
---
Section 2: Threat Model, Trust Boundaries and Assumptions
Three subsections: assets, trust boundaries, and assumptions.
Template:
## 2. Threat model, Trust boundaries and assumptions
### Assets / security goals
- {Asset 1} ({specific examples}).
- {Asset 2} ({specific examples}).
- {Asset 3} ({specific examples}).
- {Asset 4} ({specific examples}).
### Trust boundaries & input classes
**Attacker-controlled inputs**
- {Input class 1}: {specific examples with parenthetical detail about where they enter the system}.
- {Input class 2}: {specific examples}.
- {Input class 3}: {specific examples}.
**Operator-controlled inputs**
- {Input class 1}: {specific config files, env vars, paths}.
- {Input class 2}: {specific examples}.
**Developer-controlled inputs**
- {Input class 1}: {packaged assets, scripts, templates}.
- {Input class 2}: {build and test paths}.
### Assumptions & scope
- {Assumption 1 about deployment context and its security implication}.
- {Assumption 2 about what is/isn't in scope}.
- {Assumption 3 about trust relationships}.
- {Explicit scope boundary: what web/network/multi-tenant threats are out of scope and why}.Guidelines:
- Assets should be concrete, not abstract. "Host macOS integrity and user files" not "system security"
- Trust boundary entries should name specific code locations where the input enters
- Assumptions should state security implications, not just facts
---
Section 3: Attack Surface, Mitigations and Attacker Stories
One subsection per attack surface area. Number subsections (3.1, 3.2, etc.). Group by component/functional area.
Template for each subsection:
### 3.N {Component/Area Name} ({key files or modules})
**Surface:** {What code area and what inputs are involved. Name specific files, functions, or modules.}
**Risks:**
- {Risk 1: Specific vulnerability pattern with concrete detail about what goes wrong and why}.
- {Risk 2: Another risk with specific code-level detail}.
**Mitigations/controls:**
- {Existing mitigation 1: What the code already does to prevent this}.
- {Existing mitigation 2: Incidental or intentional protection}.
- {Gap or suggestion if applicable}.
**Attacker story:** {A concrete scenario: "[Actor] [does action] which causes [impact] because [code behavior]." Include preconditions (e.g., "when the tool is invoked by untrusted automation") and scope qualifiers (e.g., "in typical local usage, severity is lower").}Guidelines:
- Name specific source files in the subsection heading parenthetical
- Risks should describe the mechanism, not just the category. Not "path traversal is possible" but "bundleID is interpolated into
/tmp/agent-sim-extract/\(bundleID)without sanitization" - Mitigations include what IS there, not just what's missing
- Attacker stories must have realistic preconditions — don't assume the attacker has root if the threat model is for a local CLI
- Include an "Out-of-scope / not applicable" subsection at the end listing inapplicable threat classes
Out-of-scope template:
### Out-of-scope / not applicable
- {Threat class 1} {are not applicable because reason}.
- {Threat class 2} {is not a goal; if condition changes, these concerns become in scope}.---
Section 4: Systemic Findings
Present when pattern clustering (Phase 7) identifies 3+ findings sharing a root cause. If no systemic patterns are found, omit this section.
Template:
## 4. Systemic findings
### 4.1 {Root cause description}
**Pattern:** {vulnerability class} — {count} instances
**Root cause:** {what's missing — the abstraction, policy, or helper that would prevent all instances}
**Affected files:** {list of files with instances}
**Individual findings:** {references to Section 3 subsections}
**Recommended fix:** {single change that resolves all instances}
**Systemic severity:** {severity with justification: individual severity × count × centralizability}
### 4.2 {Another root cause}
...Guidelines:
- Only include clusters with 3+ instances — 2 findings is coincidence, not a pattern
- The recommended fix should be a single abstraction or policy, not N individual patches
- Reference the individual findings in Section 3 by their subsection numbers
- Rate systemic severity higher than any individual instance — systemic findings fix more with one change
---
Section 5: Exploit Chains
Present when chain construction (Phase 8) identifies multi-step attack paths. If no chains are found, omit this section.
Template:
## 5. Exploit chains
### Chain 1: {descriptive name}
**Path:**
1. [{Finding title}] ({individual severity}) — Attacker {action}. Gains: {what this provides}.
2. [{Finding title}] ({individual severity}) — Uses {output from step 1}. Gains: {what this provides}.
3. **Terminal impact:** {concrete outcome}
**Chain severity:** {rated by terminal impact, not weakest link}
**Preconditions:** {what must be true for the full chain}
**Chain-breaking fix:** {which single finding to fix to break this chain, and why}Guidelines:
- Maximum 4 steps per chain — longer chains are theoretical, not practical
- Each step must reference an actual finding from Section 3
- Rate by terminal impact: a chain of mediums reaching critical impact is critical
- Identify the single fix that breaks the chain — this guides remediation priority
- Include preconditions — some chains require specific deployment contexts
---
Section 6: Criticality Calibration
Group findings by severity level. Each bullet describes a specific risk with enough context to understand what it is without reading the full attack surface section.
Template:
## 6. Criticality calibration (critical, high, medium, low)
### Exploit chains
- {Chain name}: {Step 1} → {Step 2} → {terminal impact} [{chain severity}]
### Systemic findings
- {Root cause}: {count} instances, recommended fix: {single change} [{systemic severity}]
### Individual findings
**Critical**
- {Risk description with code-level specificity and data flow evidence}.
**High**
- {Risk description}. Part of systemic finding: {reference if applicable}.
**Medium**
- {Risk description}.
**Low**
- {Risk description}.Guidelines:
- Order: Chains first (highest combined impact), then systemic findings, then individual findings
- Each bullet should be self-contained — a reader should understand the risk without reading other sections
- Reference specific code areas and data flow traces where available
- Individual findings that belong to a cluster should reference their systemic parent
- Include scope qualifiers: "Critical if network-exposed; medium in internal deployments"
- End with a scope note:
**Scope note**: Findings that require {specific access condition} are {severity} only if {condition holds}. In {alternative context}, they may be downgraded to {lower severity} because {reason}.---
Formatting Conventions
- Use
##for the four main sections,###for subsections - Use
**Bold:**for field labels within subsections (Surface, Risks, Mitigations/controls, Attacker story) - Reference code with backticks: `
Sources/AgentSim/Service/ExtractionReport.swift` - Use parentheticals in subsection headings for file references:
### 3.1 CLI inputs (Sources/App/UI/, Sources/App/Service/) - Keep bullets concise — one risk per bullet, one mitigation per bullet
- Use scope qualifiers liberally: "in typical local usage", "if exposed via a service wrapper", "on shared CI machines"
Cross-Language Bridge Analysis
The hardest bugs live at language boundaries. When Swift calls C, when Ruby talks to C through Redis, when JavaScript calls WebAssembly — type systems, memory models, and error handling conventions change. This technique systematically analyzes these boundaries.
Why This Matters
agent-sim's most critical finding (use-after-free in CoreSimBridge) exists because Swift's ARC lifetime management doesn't extend into the C bridge layer. The timeout path in Swift returns and deallocates the session, while a dispatch_async block in C still holds a raw pointer to it. No single-language analysis would find this — it requires understanding both sides of the bridge.
Similarly, ab-nginx's shared-memory layout between C (NGINX module) and Ruby (Rails control plane via Redis) means a schema change on one side can corrupt the other. The trust boundary is implicit in the data format, not in an API contract.
The Technique
Step 1: Identify All Bridge Boundaries
Grep for bridge indicators:
| Bridge Type | Indicators |
|---|---|
| Swift ↔ C/ObjC | @objc, import <BridgeName.h>, withUnsafePointer, UnsafeMutablePointer, bridging headers |
| Swift ↔ C via module map | module.modulemap, .systemLibrary, clang module |
| Rust ↔ C | extern "C", #[no_mangle], unsafe { }, *const, *mut |
| Python ↔ C | ctypes, cffi, PyObject*, Cython .pyx |
| Node.js ↔ C++ | napi_, N-API, node-addon-api, .node binary modules |
| JVM ↔ C | native keyword, JNI_OnLoad, System.loadLibrary |
| Any ↔ shared memory | mmap, shm_open, shared memory zones, memory-mapped files |
| Any ↔ IPC | Redis pub/sub, Unix sockets, named pipes, D-Bus |
For each bridge, record:
- Which two languages/runtimes are connected
- Which side initiates calls (caller vs callee)
- What data types cross the boundary
- Where the bridge code lives (file paths)
Step 2: Analyze Type Crossings
At each bridge boundary, check what types cross and whether there's a mismatch:
| Check | What to Look For | Risk |
|---|---|---|
| Integer width | Swift Int (64-bit) vs C int (32-bit) | Truncation, overflow |
| Nullability | Swift optionals vs C nullable pointers | Null dereference |
| String encoding | Swift String (UTF-8) vs C char* (unknown encoding) | Encoding confusion, buffer overread |
| Array bounds | Swift Array (bounds-checked) vs C pointer (unchecked) | Buffer overflow |
| Enum representation | Swift enum (tagged union) vs C int (raw value) | Invalid state |
| Floating point | Different precision or NaN handling across languages | Trap, corruption |
| Boolean | Swift Bool vs C BOOL (signed char on ObjC) | Surprising truth values |
For each type crossing, verify: 1. Is there explicit conversion with range checking? 2. What happens on failure? (trap, silent truncation, undefined behavior) 3. Are error codes mapped correctly between languages?
Step 3: Analyze Memory Ownership
The most dangerous bridge bugs are lifetime mismatches. Check:
Who allocates, who frees?
Caller (Swift) allocates → passes pointer to callee (C) → who frees?
- If caller frees after call returns: safe IF callee doesn't store the pointer
- If callee stores the pointer: use-after-free when caller deallocates
- If callee frees: double-free if caller also freesOwnership transfer patterns:
| Pattern | Risk | Check |
|---|---|---|
| Caller allocates, callee uses synchronously | Low | Verify callee doesn't store pointer |
| Caller allocates, callee stores reference | HIGH | Verify lifetime alignment |
| Callee allocates, returns to caller | Medium | Verify caller knows to free |
| Shared allocation with reference counting | Medium | Verify atomic refcount, no races |
strdup/copy at boundary | Low | Verify both sides free their copy |
agent-sim example:
ASCoreSimSessionCreate() → allocates session struct
↓
_ResolveDeviceSet() → dispatches async block that captures session pointer
↓
Timeout fires → ASCoreSimSessionDestroy() frees session
↓
Async block still running → uses freed session → USE-AFTER-FREEThe fix was atomic reference counting (_RetainSession / _ReleaseSession) so the session lives until all users release it.
Step 4: Analyze Error Handling Across Boundaries
Errors don't translate cleanly across language boundaries:
| Check | What to Look For |
|---|---|
| Error code mapping | Does the bridge map C errno/NULL to Swift throws/Optional? |
| Exception propagation | Can an ObjC exception propagate into Swift (which doesn't catch ObjC exceptions)? |
| Partial failure | If a multi-step bridge operation fails midway, is the caller left in a consistent state? |
| Timeout handling | If the caller times out, does the callee know to stop? Or does it continue using shared state? |
| Resource cleanup on error | On error, are all allocated resources freed on BOTH sides of the bridge? |
Step 5: Analyze Async Boundaries
When async operations cross bridge boundaries, lifetime and ordering guarantees can break:
| Pattern | Risk |
|---|---|
| Caller dispatches async work on callee side, then destroys context | Use-after-free in async block |
| Callee signals completion via callback, caller has moved on | Callback into freed/invalid state |
| Shared mutable state accessed from both sides without synchronization | Data race, corruption |
| Timeout on caller side doesn't cancel work on callee side | Resource leak, stale work |
Step 6: Check Dynamic Loading
For bridges that load code dynamically (dlopen, LoadLibrary, System.loadLibrary):
1. Path validation: Is the library path fully qualified? Can an empty/relative result from xcode-select -p cause loading from attacker-controlled location? 2. Symbol verification: After loading, are expected symbols verified before use? 3. Version compatibility: Can a wrong-version library be loaded that has incompatible struct layouts? 4. Unload safety: If the library is dlclosed, are all pointers to its symbols invalidated?
Output Format for Bridge Findings
BRIDGE: {Caller Language} → {Callee Language} via {mechanism}
Files: {bridge source files}
Types crossing: {list of types that cross the boundary}
Ownership model: {who allocates, who frees, lifetime management}
Findings:
1. {Type mismatch / lifetime issue / error handling gap}
Risk: {what can go wrong}
Severity: {rating}
2. ...Common Pitfalls
- Don't assume both sides have the same error model. Swift throws, C returns NULL, ObjC throws NSException — these are three different mechanisms.
- ARC doesn't cross into C. A Swift object passed as
UnsafeRawPointerto C has no ARC protection on the C side. If C stores the pointer and Swift releases the object, it's use-after-free. - Thread safety assumptions differ. Swift actors have isolation guarantees. C code called from Swift has none unless explicitly synchronized.
- Struct layout is not guaranteed. Don't assume a Swift struct and a C struct with the same fields have the same memory layout unless explicitly bridged via
@frozenor#pragma pack.
Data Flow Tracing
The single most valuable analytical technique for finding real vulnerabilities. Instead of listing what components exist and what could theoretically go wrong, you follow specific untrusted values through actual code paths and find where they reach privileged operations without validation.
Why This Matters
Most security findings are data flow problems: an untrusted value reaches a privileged operation (file write, exec, allocation, HTML rendering) without being validated, sanitized, or bounded. The current LLM default behavior is to identify attack surfaces by component ("CLI accepts arguments, that's risky"). Data flow tracing replaces that with evidence: "this specific CLI argument flows through these specific functions and reaches this specific privileged operation with no validation checkpoint."
This is the technique that Codex uses to find findings like:
--pid→DebugAttach.run()→session.attach(pid:)→ LLDB attach to ANY process (no PID validation)bundleIDfrom CLI →/tmp/agent-sim-extract/\(bundleID)→FileManager.removeItem(path traversal)- RFC markdown →
marked.parse()→innerHTMLassignment (XSS with no sanitization)
The Technique
Step 1: Enumerate Entry Points
Use scripts/trace-data-flows.sh <project-root> to get an automated inventory, then supplement with manual reading.
Entry points are where attacker-controlled data enters the system:
| System Type | Entry Point Patterns |
|---|---|
| CLI tools | @Argument, @Option, ArgumentParser, process.argv, argparse, clap |
| Web services | req.params, req.body, req.query, req.headers, params[:key], request.GET |
| File processors | FileManager.contents, fs.readFileSync, open(), JSON.parse(fileContent) |
| IPC/messaging | NotificationCenter, WebSocket.onmessage, Redis subscribe callbacks |
| Deserialization | JSONDecoder.decode, JSON.parse, pickle.load, cJSON_Parse |
For each entry point, record:
- Variable name holding the untrusted value
- File and line where it enters
- What controls it (CLI user, HTTP client, file author, upstream service)
Step 2: Trace Each Value Forward
For each entry point variable, grep for its name and follow it through function calls.
The mechanical process: 1. Grep for the variable name in the file where it enters 2. Read each usage site — is it passed to another function? Stored in a struct? Used directly? 3. If passed to a function, read that function and repeat from the parameter name 4. At each step, note:
- Pass-through: value forwarded without change (e.g.,
self.bundleId = bundleId) - Transform: value modified (e.g.,
path = "/tmp/" + bundleId— still tainted!) - Validation: value checked (e.g.,
guard UUID(uuidString: udid) != nil— removes taint if check is sufficient) - Sink: value used in a privileged operation (see Step 3)
Key insight: Transformations do NOT remove taint. Concatenating an untrusted value into a path makes the path untrusted. Embedding an untrusted value in HTML makes the HTML untrusted. Only explicit validation (checking format, rejecting bad values, escaping for context) removes taint.
Step 3: Identify Sinks
A sink is a privileged operation where an untrusted value causes harm:
| Sink Category | Operations | Impact |
|---|---|---|
| File system | writeToFile, removeItem, createDirectory, copyItem | Overwrite, delete, traverse |
| Command execution | Process(), system(), exec(), popen(), LLDB expression | RCE |
| Memory allocation | malloc(size), [UInt8](repeating:count:), realloc | DoS via exhaustion |
| HTML rendering | innerHTML, outerHTML, document.write, template interpolation | XSS |
| SQL/query | String concatenation in queries, unparameterized WHERE | Injection |
| Network | URLSession.data(from:), fetch(), Net::HTTP.get | SSRF |
| Deserialization | NSKeyedUnarchiver, pickle.load, eval() | RCE via gadgets |
Step 4: Document the Trace
For each entry-to-sink path where the value reaches the sink without sufficient validation, document the complete trace:
TRACE: [entry-point-name]
Entry: CLI --pid argument (DebugAttach.swift:19)
↓ pass-through: stored as local `pid` (no validation)
↓ pass-through: passed to resolveSimulatorAppPID(pid:bundleId:) — BUT only when bundleId path
↓ When pid is provided directly, skips to:
Sink: session.attach(pid: resolvedPID) (DebugAttach.swift:37)
Operation: LLDB attaches to process by PID
Impact: Attaches to ANY process the user can debug, not just simulator apps
Validation: NONE between entry and sink when --pid is provided directly
FINDING: Debug attach allows arbitrary PID debugging and memory access [HIGH]Step 5: Check Validation Sufficiency
When you find a validation checkpoint, ask:
- Is it complete? Does it check all dangerous patterns? (e.g., checking for
..but not/) - Is it in the right place? Is it before or after the dangerous operation?
- Can it be bypassed? Is there another code path that reaches the same sink without this check?
- Is it context-appropriate? HTML escaping for an HTML context, path validation for a path context, not the wrong escaping for the context
Common insufficient validations:
- Escaping
<>&but not"'(attribute XSS still possible) - Checking for
..but not absolute paths - Validating format but not value range (e.g., checking UUID format but not that it belongs to a simulator)
- Checking in one code path but not in another that reaches the same sink
Working with the Trace Script
scripts/trace-data-flows.sh <project-root> outputs candidate entry-sink pairs:
## Entry Points Found
- CLI: @Option pid (Sources/AgentSim/UI/DebugGroup.swift:19)
- CLI: @Option bundleId (Sources/AgentSim/UI/DebugGroup.swift:22)
- CLI: @Option output (Sources/AgentSim/UI/Extract.swift:8)
## Sinks Found
- FileWrite: writeToFile (Sources/AgentSim/Service/ExtractionReport.swift:45)
- Exec: session.attach (Sources/AgentSim/Service/DebugSession.swift:112)
- Alloc: [UInt8](repeating:count:) (Sources/AgentSim/Service/DHParser.swift:205)
## Candidate Traces (same module)
- CLI:pid → Exec:session.attach (both in DebugGroup/DebugSession)
- CLI:output → FileWrite:writeToFile (both in Extract/ExtractionReport)Use these candidates as starting points for manual tracing. The script finds correlations; you verify causation by reading the actual code between entry and sink.
Common Pitfalls
- Don't stop at the first function boundary. The value often passes through 3-5 functions before reaching a sink. Follow it all the way.
- Transforms preserve taint.
"/tmp/" + userInputis still tainted.URL(string: userInput)is still tainted. Only explicit validation removes taint. - Watch for aliasing. The value may be stored in a struct field, then accessed later under a different name. Trace through data structures.
- Check ALL paths to the sink. A function may have a validated path and an unvalidated path (e.g.,
if let bundleId { validated } else { unvalidated }). - Don't trace developer-controlled inputs. Focus on attacker-controlled and operator-controlled tiers. Tracing source code constants wastes time.
Incremental / Diff-Based Analysis
Analyze what changed, not the whole codebase. Accept a git range and focus on newly introduced attack surface. This is how Codex operates — it scans each commit for security regressions rather than re-analyzing the entire project.
When to Use
- PR review: "Is this change safe?"
- Post-incident: "What changed between the last known-good state and now?"
- Periodic review: "What new attack surface was added this sprint?"
- Regression check: "Did this security fix introduce any new issues?"
The Technique
Step 1: Get the Diff
# Last N commits
git diff HEAD~10..HEAD --stat
git diff HEAD~10..HEAD -- '*.swift' '*.c' '*.m' '*.js' '*.ts' '*.rb' '*.py'
# Specific PR/branch
git diff main...feature-branch --stat
# Between tags
git diff v1.0..v1.1 --statFocus on source files, not generated/vendored code. Use --stat first to identify which files changed, then read the actual diffs for security-relevant files.
Step 2: Classify Changes
For each changed file, classify the change:
| Classification | What It Means | Analysis Priority |
|---|---|---|
| New entry point | New CLI command, HTTP endpoint, file parser | HIGH — trace all new inputs |
| New sink | New file write, exec call, HTML render | HIGH — check what reaches it |
| Modified validation | Changed input validation or escaping | HIGH — check if weakened |
| New bridge/FFI | New cross-language boundary | HIGH — full bridge analysis |
| New dependency | Added library or framework | MEDIUM — check for known CVEs |
| Removed validation | Deleted or relaxed a security check | CRITICAL — why was it removed? |
| Refactored code path | Same logic, different structure | MEDIUM — check data flow preserved |
| New data model | New struct/class handling untrusted data | MEDIUM — check field validation |
| Config change | Modified deployment, auth, or security config | MEDIUM — check for weakening |
| Test/doc only | No runtime code changed | LOW — skip unless test reveals intent |
Step 3: Trace New Data Flows
For each new entry point or sink introduced in the diff:
1. If new entry point: Trace forward to find what sinks it can reach. Use the data flow tracing technique from data-flow-tracing.md. 2. If new sink: Trace backward to find what entry points can reach it. Grep for callers of the function containing the sink. 3. If modified validation: Check if the modification weakened the validation. Compare old vs new: does the new version accept inputs the old version rejected?
Step 4: Cross-Reference with Existing Threat Model
If a THREAT-MODEL.md exists from a previous analysis:
1. New surfaces: Findings in the diff that weren't in the previous model → add them 2. Resolved surfaces: Previous findings whose vulnerable code was fixed in the diff → mark resolved 3. Modified surfaces: Previous findings whose code changed but vulnerability status is unclear → re-analyze 4. Regression: A fix for one finding that introduces a new finding → flag as regression
Step 5: Check for Common Regression Patterns
| Pattern | What to Check |
|---|---|
| Fix moved the bug | Did the fix eliminate the vulnerability or just move it to a different code path? |
| Fix introduced new entry point | Did the fix refactor code in a way that exposes a new entry point? |
| Fix weakened existing control | Did the fix disable or relax a validation that was protecting against something else? |
| New feature copies vulnerable pattern | Did new code copy-paste from code with known issues? |
| Dependency update | Did the update change behavior in security-relevant ways? |
Step 6: Output as Delta
Structure the output as changes to the threat model:
## Diff Analysis: {git range}
### Files Changed: {count}
### Security-Relevant Changes: {count}
### New Findings
{Findings introduced by this change}
### Resolved Findings
{Previous findings fixed by this change}
### Modified Findings
{Previous findings whose status changed}
### Regressions
{New issues introduced by security fixes}Practical Tips
- Start with `--stat` to identify which files changed, then prioritize by classification
- Skip vendored/generated code — filter with
git diff -- ':!vendor' ':!node_modules' ':!*.min.js' - Read the commit messages — they often explain intent, which helps distinguish intentional security changes from accidental ones
- Check for reverted security fixes —
git log --all --grep="security"to find previous security commits, then check if any were reverted in the diff range - Large diffs (100+ files): Focus only on files classified as HIGH priority in Step 2. Don't try to analyze everything.
Exploit Chain Construction
Individual findings rated medium can combine into chains rated critical. This technique identifies prerequisite relationships between findings and models multi-step attack paths.
Why This Matters
Security scanners rate findings individually. But real attacks chain vulnerabilities: an info disclosure reveals a file path, a path traversal uses that path to write to a predictable location, and a symlink race at that location redirects the write to an arbitrary target. Each step is medium; the chain is critical.
In agent-sim's findings:
- Path traversal via bundleID (medium) + predictable
/tmp/agent-sim-extract/<bundleID>(medium) + no symlink checks (medium) = arbitrary file overwrite with attacker-influenced content (critical) - Manifest filename traversal (medium) +
createDirectory(withIntermediateDirectories: true)(medium) = arbitrary directory creation + file overwrite via manifest (high)
The Technique
Step 1: Model Each Finding as Input/Output
For every finding from Phase 6, identify:
What the finding PROVIDES (output):
- Information (file paths, PIDs, internal state, credentials)
- Access (read a file, write a file, execute code, create a directory)
- State change (modify config, change permissions, escalate privilege)
What the finding REQUIRES (input):
- Information (need to know a path, PID, or token)
- Access (need local access, network access, or authenticated session)
- Precondition (need a specific config, timing window, or state)
Template per finding:
Finding: {title}
Provides: {what an attacker gains from exploiting this}
Requires: {what preconditions must hold for exploitation}
Individual severity: {rating}Step 2: Find Prerequisite Matches
A chain exists when Finding A's output satisfies Finding B's input:
Finding A provides → {information/access/state}
↓ (matches)
Finding B requires → {information/access/state}Scan all findings pairwise. For N findings, check N×(N-1) pairs. In practice, most pairs don't connect — focus on findings in the same component or data flow.
Common chain patterns:
| Chain Type | Step 1 Provides | Step 2 Requires | Terminal Impact |
|---|---|---|---|
| Info → Traversal | File path or internal ID | Known path to target | Read/write arbitrary files |
| Traversal → Write | Directory escape | Writable location | File overwrite outside intended scope |
| Write → Exec | File at controlled path | File that gets executed | Code execution |
| Info → Auth bypass | Credentials or tokens | Valid authentication | Unauthorized access |
| DoS → Race | Process crash/restart | Timing window | Exploit during restart |
| Config → Poison | Ability to modify config | Config read on startup | Persistent compromise |
Step 3: Document the Chain
For each identified chain, document the full path:
CHAIN: {descriptive name}
Path:
1. [Finding A: {title}] ({severity})
Attacker: {action taken}
Gains: {what this provides}
↓
2. [Finding B: {title}] ({severity})
Uses: {what from step 1}
Attacker: {action taken}
Gains: {what this provides}
↓
3. [Terminal Impact]
Result: {concrete outcome}
Chain severity: {rated by terminal impact}
Preconditions: {what must be true for the full chain to work}
Likelihood: {how realistic given the preconditions}Step 4: Rate Chain Severity
Rate by terminal impact, not weakest link. If a chain of three medium findings results in arbitrary code execution, the chain is critical.
However, adjust for precondition realism:
| Preconditions | Adjustment |
|---|---|
| All steps reachable by unauthenticated attacker | No adjustment |
| Requires local access | Down one level unless local access is in-scope |
| Requires specific timing window | Down one level |
| Requires compromised upstream component | Down one level (and note the dependency) |
| Requires physical access | Down two levels |
A chain rated critical after precondition adjustment is genuinely critical and should be the top priority for remediation.
Step 5: Identify Chain-Breaking Controls
For each chain, identify which single fix would break the chain:
Chain-breaking fix: {which finding to fix first}
Rationale: {why fixing this step prevents the entire chain}The most valuable fix is the one that breaks the most chains. If Finding B appears in 3 different chains, fixing B breaks all three.
Chain Construction Rules
1. Maximum 4 steps. Longer chains are theoretical, not practical. If you need 5+ steps, the attack is unrealistic. 2. Each step must be independently exploitable. Don't invent intermediate steps that aren't actual findings. 3. Preconditions must be achievable. "Attacker needs root access" as a precondition for step 1 makes the chain moot. 4. Don't double-count. If two findings are really the same vulnerability in different files, they don't form a chain — they form a cluster (use pattern clustering instead). 5. Chains should cross components. A chain within a single function is really one finding. Chains are valuable when they cross trust boundaries or components.
Interaction with Pattern Clustering
Chains and clusters are different lenses on the same findings:
- Cluster: "These 5 findings share a root cause" (horizontal grouping)
- Chain: "These 3 findings combine into a worse outcome" (vertical composition)
A finding can belong to both a cluster AND a chain. Document both — they serve different purposes. Clusters guide remediation priority (fix the root cause). Chains guide severity assessment (this combination is worse than the parts).
Pattern Clustering
After enumerating individual attack surfaces, step back and look for patterns. When 3+ findings share a root cause, that's a systemic weakness worth more attention than any individual finding.
Why This Matters
Individual findings are symptoms. Patterns reveal the disease. Codex found 8 predictable-tmp findings in agent-sim — each medium severity. But the root cause is a single architectural gap: the project has no secure temporary directory abstraction. Fixing that one gap resolves all 8 findings. Rating and recommending systemic fixes is more valuable than listing 8 individual patches.
The Technique
Step 1: Categorize Findings by Vulnerability Class
After Phase 6 (Attack Surface Enumeration), tag each finding with its vulnerability class:
| Class | Pattern | Example |
|---|---|---|
PREDICTABLE_TMP | Fixed paths under /tmp without unique naming | /tmp/agent-sim-extract/<bundleID> |
PATH_TRAVERSAL | Unsanitized user input in path construction | bundleID with ../ in path concatenation |
SYMLINK_RACE | File ops at predictable paths without link checks | writeToFile at /tmp/known-name.json |
XSS_NO_SANITIZE | Untrusted data in HTML without escaping | marked.parse() → innerHTML |
INJECTION | Untrusted input interpolated into commands/queries | String interpolation into LLDB expressions |
UNBOUNDED_ALLOC | Allocation sized by untrusted value without cap | realloc(buf, untrusted_size) |
LIFETIME_RACE | Resource used after owning scope ends | Async block using freed session |
MISSING_AUTH | Mutation endpoint without auth middleware | Rails controller without before_action |
INFO_DISCLOSURE | Internal data exposed without access control | Status endpoints returning telemetry |
Step 2: Count Instances Per Class
| Class | Count | Files |
|---|---|---|
| PREDICTABLE_TMP | 8 | ExtractionCapture, ExtractionCatalog, LLDBExtractor, ... |
| XSS_NO_SANITIZE | 4 | ExtractionReport, DesignHTMLBuilder, viewer.html |
| PATH_TRAVERSAL | 3 | AppResigner, Update, Manifest |
| SYMLINK_RACE | 6 | ExtractionCapture, ScreenExtractor, ... |
Step 3: Identify Root Causes (groups with 3+ instances)
For each group with 3+ findings, identify the shared root cause — the missing abstraction, policy, or validation that if added would resolve all instances:
Template:
SYSTEMIC FINDING: {root cause description}
Instances: {count} individual findings
Root cause: {what's missing — the abstraction, policy, or helper}
Recommended fix: {single change that resolves all instances}
Affected files: {list}
Systemic severity: {see rating below}Example:
SYSTEMIC FINDING: No secure temporary directory abstraction
Instances: 8 individual findings (PREDICTABLE_TMP × 5, SYMLINK_RACE × 3)
Root cause: Every component creates its own temp paths using hardcoded `/tmp/`
prefixes. No shared helper enforces unique naming, restrictive permissions,
or cleanup.
Recommended fix: Create a `secureTemporaryDirectory(prefix:)` function that:
1. Uses `mkdtemp` or UUID-based naming under NSTemporaryDirectory()
2. Sets permissions to 0700
3. Returns the path for use, with cleanup handled by the caller
Replace all 8 hardcoded /tmp/ paths with calls to this helper.
Affected files: ExtractionCapture.swift, ExtractionCatalog.swift,
LLDBExtractor.swift, AppResigner.swift, ScreenExtractor.swift
Systemic severity: HIGH (8 instances × medium individual severity × high centralizability)Step 4: Rate Systemic Severity
Systemic findings get a severity boost based on:
| Factor | Low | Medium | High |
|---|---|---|---|
| Instance count | 3-4 | 5-7 | 8+ |
| Individual severity | Low | Medium | High/Critical |
| Fix centralizability | Hard (different root causes) | Moderate (shared pattern) | Easy (one helper fixes all) |
Rating formula: Take the highest individual severity, then adjust up if:
- 5+ instances AND high centralizability → bump one level (medium → high)
- 8+ instances AND the pattern is expanding (new instances in recent commits) → bump one level
A systemic finding should never be rated LOWER than its highest individual instance.
Step 5: Distinguish Systemic from Individual
In the output, present systemic findings BEFORE individual findings in the Criticality Calibration section. Systemic findings are more important because: 1. They fix multiple vulnerabilities with one change 2. They prevent future instances of the same pattern 3. They indicate an architectural gap, not just a bug
Individual findings that belong to a cluster should reference their systemic parent:
- Path traversal via bundleID in AppResigner.swift [medium]
→ Part of systemic finding: "No input validation for path components"When NOT to Cluster
- 2 findings: Not enough for a pattern. List individually.
- Different root causes: 3 XSS findings might have different root causes (one is missing escaping, another is missing CSP, a third is using
eval). Only cluster if the fix is genuinely shared. - Different components: Findings in completely separate subsystems that happen to share a vulnerability class but don't share code paths. These are coincidences, not systemic issues.
#!/usr/bin/env bash
# scan-patterns.sh — Scan a codebase for security-relevant code patterns
# Gives the threat modeler a head start by surfacing code that commonly
# correlates with security issues. NOT a vulnerability scanner — just
# pattern matching to guide manual analysis.
#
# Usage: scan-patterns.sh <project-root>
# Output: Grouped findings by category, with file:line references
set -euo pipefail
command -v rg >/dev/null 2>&1 || {
echo "Error: ripgrep (rg) is required but not installed." >&2
echo "Install via: brew install ripgrep (macOS), apt install ripgrep (Debian/Ubuntu)" >&2
echo "See: https://github.com/BurntSushi/ripgrep#installation" >&2
exit 1
}
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <project-root>" >&2
echo "Scans for security-relevant code patterns to guide threat modeling." >&2
exit 1
fi
ROOT="$1"
if [[ ! -d "$ROOT" ]]; then
echo "Error: '$ROOT' is not a directory" >&2
exit 1
fi
# Exclusions: vendored code, build output, test fixtures, node_modules
EXCLUDE="--glob=!vendor --glob=!node_modules --glob=!.build --glob=!build --glob=!dist --glob=!Pods --glob=!.git --glob=!*.min.js --glob=!package-lock.json --glob=!yarn.lock"
scan() {
local label="$1"
local pattern="$2"
shift 2
local results
results=$(rg -n --no-heading $EXCLUDE "$@" "$pattern" "$ROOT" 2>/dev/null || true)
if [[ -n "$results" ]]; then
echo "### $label"
echo "$results" | head -20
local count
count=$(echo "$results" | wc -l | tr -d ' ')
if [[ "$count" -gt 20 ]]; then
echo " ... and $((count - 20)) more matches"
fi
echo ""
fi
}
echo "# Security Pattern Scan: $(basename "$ROOT")"
echo "# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
echo "## Predictable Temporary Paths"
scan "Hardcoded /tmp paths" '/tmp/' --glob='!*.md' --glob='!*.txt'
echo "## Path Construction"
scan "Path interpolation" '(appendingPathComponent|path\.join|Path\.Combine|os\.path\.join)' --glob='!*.md'
scan "Potential traversal" '\.\.\/' --glob='!*.md' --glob='!*.lock'
echo "## HTML / Template Injection"
scan "innerHTML usage" 'innerHTML' --glob='!*.md'
scan "Script tag embedding" '(<script|</script)' --glob='*.html' --glob='*.erb' --glob='*.ejs'
scan "Marked/markdown rendering" '(marked\.parse|marked\()' --glob='*.js' --glob='*.ts'
scan "dangerouslySetInnerHTML" 'dangerouslySetInnerHTML'
echo "## Command / Expression Injection"
scan "Process/exec calls" '(Process\(|NSTask|system\(|popen\(|exec\(|child_process)' --glob='!*.md'
scan "eval usage" '(eval\(|Function\()' --glob='*.js' --glob='*.ts' --glob='*.py'
scan "Shell interpolation" '(\$\(|`.*`)' --glob='*.sh'
echo "## Native Code / Unsafe Operations"
scan "Unsafe pointer access" '(withUnsafeBytes|withUnsafePointer|UnsafeRawPointer|UnsafeBufferPointer)' --glob='*.swift'
scan "C memory allocation" '(malloc|calloc|realloc|free\()' --glob='*.c' --glob='*.m' --glob='*.mm' --glob='*.cpp'
scan "dlopen/dlsym" '(dlopen|dlsym|dlclose)' --glob='*.c' --glob='*.m' --glob='*.mm' --glob='*.swift'
scan "fromByteOffset" 'fromByteOffset' --glob='*.swift'
scan "String(cString:)" 'String\(cString' --glob='*.swift'
echo "## Credential / Secret Patterns"
scan "Hardcoded secrets" '(SECRET_KEY|API_KEY|PASSWORD|PRIVATE_KEY|Bearer )' -i --glob='!*.md' --glob='!*.lock'
scan "Hardcoded URLs with auth" '(https?://[^@\s]*:[^@\s]*@)' --glob='!*.md'
echo "## Serialization / Parsing"
scan "JSON parsing" '(JSONSerialization|cJSON_Parse|JSON\.parse|json\.loads)' --glob='!*.md'
scan "Decompression" '(gunzip|inflate|decompress|Compression)' --glob='!*.md'
echo "## Authentication / Authorization"
scan "Auth-related" '(authenticate|authorize|before_action|middleware.*auth)' --glob='!*.md' --glob='!node_modules'
scan "config.hosts.clear" 'config\.hosts\.clear'
scan "CORS permissive" '(Access-Control-Allow-Origin.*\*|cors.*origin.*true)' --glob='!*.md'
echo "## File Operations"
scan "File deletion" '(removeItem|unlink\(|rm -|File\.delete)' --glob='!*.md' --glob='!*.sh'
scan "File permissions" '(chmod|0777|0755|0750|permissions)' --glob='!*.md'
scan "Symlink operations" '(createSymbolicLink|isSymbolicLink|readlink|lstat)' --glob='!*.md'
echo "---"
echo "# Summary"
echo "This scan highlights code patterns that commonly correlate with security"
echo "issues. Each match needs manual review to determine if it represents an"
echo "actual vulnerability in context. Use these results to guide attack surface"
echo "enumeration, not as a findings list."
#!/usr/bin/env bash
# trace-data-flows.sh — Find entry points and sinks, suggest candidate traces
# Gives the analyst structured data about where untrusted input enters and
# where privileged operations occur. The agent then verifies connections manually.
#
# Usage: trace-data-flows.sh <project-root> [--language <swift|c|js|py|rb|go|rust>]
# Output: Entry points, sinks, and candidate traces by proximity
set -euo pipefail
command -v rg >/dev/null 2>&1 || {
echo "Error: ripgrep (rg) is required. Install: brew install ripgrep" >&2
exit 1
}
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <project-root> [--language <swift|c|js|py|rb|go|rust>]" >&2
exit 1
fi
ROOT="$1"
shift
LANG_FILTER=""
while [[ $# -gt 0 ]]; do
case "$1" in
--language)
if [[ $# -lt 2 ]]; then
echo "Error: --language requires a value (swift|c|js|py|rb|go|rust)" >&2
exit 1
fi
LANG_FILTER="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
EXCLUDE="--glob=!vendor --glob=!node_modules --glob=!.build --glob=!build --glob=!dist --glob=!Pods --glob=!.git --glob=!*.min.js --glob=!*lock*"
# Language-specific glob filters
case "$LANG_FILTER" in
swift) GLOB="--glob=*.swift" ;;
c) GLOB="--glob=*.{c,m,mm,h,cpp}" ;;
js) GLOB="--glob=*.{js,ts,jsx,tsx}" ;;
py) GLOB="--glob=*.py" ;;
rb) GLOB="--glob=*.rb" ;;
go) GLOB="--glob=*.go" ;;
rust) GLOB="--glob=*.rs" ;;
"") GLOB="" ;;
*) echo "Warning: unknown language '$LANG_FILTER', scanning all file types" >&2; GLOB="" ;;
esac
scan_entries() {
local label="$1" pattern="$2"
local results
results=$(rg -n --no-heading $EXCLUDE $GLOB "$pattern" "$ROOT" 2>/dev/null || true)
if [[ -n "$results" ]]; then
echo "### $label"
echo "$results" | head -30
local count
count=$(echo "$results" | wc -l | tr -d ' ')
if [[ "$count" -gt 30 ]]; then
echo " ... and $((count - 30)) more"
fi
echo ""
fi
}
echo "# Data Flow Analysis: $(basename "$ROOT")"
echo "# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
echo "## Entry Points (where untrusted data enters)"
echo ""
# CLI argument parsing
scan_entries "CLI Arguments (Swift ArgumentParser)" '@(Argument|Option|Flag)\b'
scan_entries "CLI Arguments (Node.js)" '(process\.argv|commander\.|yargs\.|minimist)'
scan_entries "CLI Arguments (Python)" '(argparse|sys\.argv|click\.)'
scan_entries "CLI Arguments (Go)" '(flag\.String|os\.Args|cobra)'
scan_entries "CLI Arguments (Rust)" '(clap::Arg|structopt|std::env::args)'
# HTTP request parameters
scan_entries "HTTP Params (Express/Node)" '(req\.(params|body|query|headers|cookies)\[|req\.get\()'
scan_entries "HTTP Params (Rails)" '(params\[|params\.permit|params\.require|params\.expect)'
scan_entries "HTTP Params (Python)" '(request\.(GET|POST|json|data|args|form)\[)'
scan_entries "HTTP Params (Go)" '(r\.URL\.Query|r\.FormValue|r\.Header\.Get)'
# File content reads (untrusted file data)
scan_entries "File Reads" '(readFileSync|readFile\(|contents\(atPath|contentsOfDirectory|open\(.+["\x27]r)'
scan_entries "JSON/Data Parsing" '(JSON\.parse|JSONDecoder|JSONSerialization|cJSON_Parse|json\.loads|json\.load)'
# Environment variables
scan_entries "Environment Variables" '(ProcessInfo\.processInfo\.environment|process\.env\.|os\.environ|os\.Getenv)'
echo "---"
echo ""
echo "## Sinks (where privileged operations occur)"
echo ""
# File system writes
scan_entries "File Writes" '(write\(toFile|writeFileSync|writeFile\(|createDirectory|removeItem|copyItem|moveItem|fs\.unlink|fs\.rm)'
scan_entries "File Path Construction" '(appendingPathComponent|path\.join|Path\.Combine|os\.path\.join)'
# Command/code execution
scan_entries "Command Execution" '(Process\(|NSTask|system\(|popen\(|exec\(|child_process|spawn\()'
scan_entries "Dynamic Evaluation" '(eval\(|Function\(|expression\s+--|dlopen|dlsym)'
# Memory allocation from untrusted sizes
scan_entries "Sized Allocation" '(malloc\(|calloc\(|realloc\(|\[UInt8\]\(repeating|Buffer\.alloc|new ArrayBuffer)'
# HTML/template rendering
scan_entries "HTML Rendering" '(innerHTML|outerHTML|document\.write|marked\.parse|dangerouslySetInnerHTML)'
scan_entries "Template Interpolation" '(render\(|erb|ejs|pug|handlebars|mustache)'
# SQL/database
scan_entries "SQL Queries" '(SELECT.*FROM|INSERT.*INTO|UPDATE.*SET|DELETE.*FROM|\.execute\(|\.query\()' -i
# Network requests (SSRF surface)
scan_entries "Outbound HTTP" '(URLSession|fetch\(|Net::HTTP|requests\.(get|post)|http\.Get|curl)'
echo "---"
echo ""
echo "## Candidate Traces"
echo ""
echo "The following entry-sink pairs appear in the same source directory or module."
echo "Each candidate needs manual verification: read the code between entry and sink"
echo "to determine if the untrusted value actually flows to the privileged operation."
echo ""
# Collect entry-point files and sink files, find overlaps
ENTRY_FILES=$(mktemp)
SINK_FILES=$(mktemp)
trap 'rm -f "$ENTRY_FILES" "$SINK_FILES"' EXIT
# Collect unique directories containing entry points
rg -l $EXCLUDE $GLOB '@(Argument|Option)|process\.argv|argparse|params\[|req\.(params|body|query)|request\.(GET|POST)|readFileSync|contents\(atPath|JSON\.parse|JSONDecoder|cJSON_Parse' "$ROOT" 2>/dev/null | while read -r f; do
dirname "$f"
done | sort -u > "$ENTRY_FILES"
# Collect unique directories containing sinks
rg -l $EXCLUDE $GLOB 'write\(toFile|writeFileSync|removeItem|copyItem|Process\(|system\(|popen\(|exec\(|dlopen|malloc\(|realloc\(|innerHTML|\.execute\(' "$ROOT" 2>/dev/null | while read -r f; do
dirname "$f"
done | sort -u > "$SINK_FILES"
# Find directories that contain BOTH entries and sinks
OVERLAPS=$(comm -12 "$ENTRY_FILES" "$SINK_FILES")
if [[ -n "$OVERLAPS" ]]; then
echo "### Directories with both entry points and sinks"
echo ""
echo "$OVERLAPS" | while read -r dir; do
rel=$(python3 -c "import os.path; print(os.path.relpath('$dir', '$ROOT'))" 2>/dev/null || echo "$dir")
entries=$(rg -c $EXCLUDE $GLOB '@(Argument|Option)|process\.argv|argparse|params\[|req\.(params|body|query)|request\.(GET|POST)|readFileSync|contents\(atPath|JSON\.parse|JSONDecoder|cJSON_Parse' "$dir" 2>/dev/null | wc -l | tr -d ' ')
sinks=$(rg -c $EXCLUDE $GLOB 'write\(toFile|writeFileSync|removeItem|copyItem|Process\(|system\(|popen\(|exec\(|dlopen|malloc\(|realloc\(|innerHTML|\.execute\(' "$dir" 2>/dev/null | wc -l | tr -d ' ')
echo "- **$rel** — $entries entry-point files, $sinks sink files"
done
echo ""
echo "Start tracing in these directories: they have the highest probability of"
echo "untrusted data reaching privileged operations."
else
echo "No directory overlaps found. Entry points and sinks are in separate modules."
echo "Trace data flow across module boundaries by following function call chains."
fi
Related skills
FAQ
What does threat-model do?
threat-model: A skill for development. This provides functionality for development workflows.
When should I use threat-model?
When you need to use threat-model for development tasks, or when threat-model: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
threat-model.