Generated banner for Graph Evolution
Generated logo for Graph Evolution

Graph Evolution

Trail of BitsOfficialPublished by Trail of Bits

  • Skill
  • Workflow
  • Security
  • CC-BY-SA-4.0
  • Low risk
  • Has scripts

From the author

Compares Trailmark code graphs at two source code snapshots (git commits, tags, or directories) to surface security-relevant structural changes. Detects new attack paths, complexity shifts, blast radius growth, taint propagation changes, and privilege boundary modifications that text diffs miss. Use when comparing code between commits or tags, analyzing structural evolution, detecting attack surface growth, reviewing what changed between audit snapshots, or finding security-relevant changes that text diffs miss.
— the description by Trail of Bits

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Graph Evolution performs a structural comparison of source code snapshots using Trailmark graphs. Unlike traditional text-based diffs, this skill identifies security-critical changes such as new attack paths, modifications to privilege boundaries, shifts in taint propagation, and growth in blast radius or cyclomatic complexity. It is designed to provide deep structural context for security audits and code reviews by comparing two git refs or directories.

What it does

  • Builds and compares Trailmark code graphs for two distinct code snapshots.
  • Detects structural changes including new attack paths and privilege boundary modifications.
  • Identifies shifts in taint propagation and blast radius for sensitive functions.
  • Tracks cyclomatic complexity evolution across code versions.
  • Generates a structured markdown report detailing security-relevant structural findings.
  • Cross-references structural findings with git diffs for source-level context.

When to use it

  • Comparing two git refs to understand structural security evolution.
  • Auditing a range of commits for security-relevant structural changes.
  • Detecting new attack paths or entrypoints introduced by code changes.
  • Identifying functions where blast radius or complexity has grown significantly.
  • Verifying that security controls remain intact during refactoring.

Advantages

  • Surfaces security-relevant structural changes that text diffs miss.
  • Provides automated analysis of taint propagation and privilege boundaries.
  • Enables objective tracking of complexity and blast radius growth.
  • Supports security-focused triage by classifying findings by severity.
  • Integrates with other security tools for comprehensive audit workflows.

Usage examples

“Compare the structural security changes between the v1.2.0 and v1.3.0 tags in my repository.”
The agent will create git worktrees for both tags, build Trailmark graphs, compute the structural and subgraph diffs, and generate a markdown report highlighting any new attack paths or privilege boundary changes.
“Analyze the structural evolution of my backend service between the main and develop branches.”
The agent will generate a report identifying functions with increased cyclomatic complexity, new entrypoints, and any changes in taint propagation that could impact the security posture.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/trailofbits/graph-evolution/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip graph-evolution.zip -d .agents/skills

Documentation by Trail of Bits

Graph Evolution

Builds Trailmark code graphs at two source snapshots and computes a structural diff. Surfaces security-relevant changes that text-level diffs miss: new attack paths, complexity shifts, blast radius growth, taint propagation changes, and privilege boundary modifications.

When to Use

  • Comparing two git refs to understand what structurally changed

  • Auditing a range of commits for security-relevant evolution

  • Detecting new attack paths created by code changes

  • Finding functions whose blast radius or complexity grew silently

  • Identifying taint propagation changes across refactors

  • Pre-release structural comparison (tag-to-tag or branch-to-branch)

When NOT to Use

  • Line-level code review (use differential-review for text-diff analysis)

  • Single-snapshot analysis (use the trailmark skill directly)

  • Diagram generation from a single snapshot (use the diagramming-code skill)

  • Mutation testing triage (use the genotoxic skill)

Rationalizations to Reject

RationalizationWhy It's WrongRequired Action
"We just need the structural diff, skip pre-analysis"Without pre-analysis, you miss taint changes, blast radius growth, and privilege boundary shiftsRun engine.preanalysis() on both snapshots
"Text diff covers what changed"Text diffs miss new attack paths, transitive complexity shifts, and subgraph membership changesUse structural diff to complement text diff
"Only added nodes matter"Removed security functions and shifted privilege boundaries are equally dangerousReview removals and modifications, not just additions
"Low-severity structural changes can be ignored"INFO-level changes (dead code removal) can mask removed security checksClassify every change, review removals for replaced functionality
"One snapshot's graph is enough for comparison"Single-snapshot analysis can't detect evolution — you need both before and afterAlways build and export both graphs
"Tool isn't installed, I'll compare manually"Manual comparison misses what graph analysis catchesInstall trailmark first
"The diff came back empty, so nothing changed structurally"trailmark diff defaults --language to python and exits 0 with empty arrays on any other target, so an empty diff reads identically whether the code is unchanged or the language was wrongPass --language explicitly and re-run before concluding no change

Prerequisites

trailmark must be installed. If uv run trailmark fails, run:

uv tool install trailmark
# Python snippets: uv run --with trailmark python -   (a tool env is not importable)

DO NOT fall back to "manual comparison" or reading source files as a substitute for running trailmark. The tool must be installed and used programmatically. If installation fails, report the error.


Quick Start

# Compare two git refs (e.g., tags, branches, commits)
# 1. Build graphs at each snapshot
# 2. Run pre-analysis on both
# 3. Compute structural diff
# 4. Generate report

# Step-by-step: see Workflow below

Decision Tree

├─ Need to understand what each metric means?
│  └─ Read: references/evolution-metrics.md
│
├─ Need the report output format?
│  └─ Read: references/report-format.md
│
├─ Already have two graph JSON exports?
│  └─ Jump to Phase 3 (run native diff + graph_diff.py)
│
└─ Starting from two git refs?
   └─ Start at Phase 1

Workflow

Graph Evolution Progress:
- [ ] Phase 1: Create snapshots (git worktrees)
- [ ] Phase 2: Build graphs + pre-analysis on both snapshots
- [ ] Phase 3: Compute structural diff
- [ ] Phase 4: Interpret diff and generate report
- [ ] Phase 5: Clean up worktrees
Phase 1: Create Snapshots

Use git worktrees to get clean copies of each ref without disturbing the working tree.

# Create temp directories for worktrees
BEFORE_DIR=$(mktemp -d)
AFTER_DIR=$(mktemp -d)

# Create worktrees (run from repo root)
git worktree add "$BEFORE_DIR" {before_ref}
git worktree add "$AFTER_DIR" {after_ref}

If comparing two directories instead of git refs, skip this phase and use the directory paths directly in Phase 2.

Phase 2: Build Graphs and Run Pre-Analysis

Build Trailmark graphs for both snapshots and run pre-analysis on each. Pre-analysis computes blast radius, taint propagation, privilege boundaries, and entrypoint enumeration.

from trailmark.query.api import QueryEngine

def build_and_export(target_dir, output_path, language="auto"):
    """Build graph, run pre-analysis, export JSON."""
    engine = QueryEngine.from_directory(target_dir, language=language)
    engine.preanalysis()
    json_str = engine.to_json()
    with open(output_path, "w") as f:
        f.write(json_str)
    return engine.summary()

import tempfile, os
work_dir = tempfile.mkdtemp(prefix="trailmark_evolution_")
before_json = os.path.join(work_dir, "before_graph.json")
after_json = os.path.join(work_dir, "after_graph.json")

before_summary = build_and_export(
    "{before_dir}", before_json
)
after_summary = build_and_export(
    "{after_dir}", after_json
)

Verify both graphs built successfully by checking the summary output. If either fails, rerun with an explicit language or comma-separated list instead of auto.

Phase 3: Compute Structural Diff

Run both:

  1. Trailmark's native structural diff for nodes, edges, and entrypoints

  2. The plugin's graph_diff.py helper for subgraph membership changes

Use the same work_dir from Phase 2, and pass the same --language value Phase 2 built with. trailmark diff defaults that flag to python, so on any other target the default exits 0 and writes empty arrays rather than reporting a mismatch.

trailmark diff --json --language auto "{before_dir}" "{after_dir}" > "{work_dir}/trailmark_diff.json" || \
  uv run trailmark diff --json --language auto "{before_dir}" "{after_dir}" > "{work_dir}/trailmark_diff.json"

uv run {baseDir}/scripts/graph_diff.py \
    --before "{before_json}" \
    --after "{after_json}" > "{work_dir}/subgraph_diff.json"

If Phase 2 needed an explicit language or a comma-separated list instead of auto, use that same value here.

If either diff command fails or writes an empty JSON file, stop and report the error instead of continuing to Phase 4.

A trailmark_diff.json whose nodes, edges, and entrypoints arrays are all empty means either nothing changed structurally or both snapshots parsed to (near-)empty graphs. Decide which using Phase 2's graph summaries: if either snapshot's node count is zero or implausibly small for the target, the parse missed the code — name the language set explicitly (rust, solidity, python,rust) and re-run. Healthy node counts on both snapshots plus an empty diff is genuine structural stability.

The native Trailmark diff contains:

KeyContents
summary_deltaChanges in node/edge/entrypoint counts
nodes.addedNew functions, classes, methods
nodes.removedDeleted functions, classes, methods
nodes.modifiedFunctions with changed CC, params, line span
edges.addedNew call/inheritance/import relationships
edges.removedDeleted relationships
entrypointsAdded, removed, and modified entrypoints

The subgraph diff contains:

KeyContents
subgraphsPer-subgraph membership changes (tainted, high_blast_radius, etc.)
Phase 4: Interpret Diff and Generate Report

Read both diff JSON files and generate a security-focused markdown report. See references/report-format.md for the full template.

Interpretation priorities (highest to lowest):

  1. New tainted paths — nodes entering the tainted subgraph, especially if they also appear in added edges targeting sensitive functions

  2. Privilege boundary changes — new or removed trust transitions from the native entrypoint/edge diff plus the subgraph diff

  3. Attack surface growth — new entrypoints, especially untrusted_external, from trailmark_diff.json

  4. Blast radius increases — nodes entering high_blast_radius

  5. Complexity spikes — CC increases > 3 on tainted or entrypoint-reachable nodes

  6. Structural additions — new nodes and edges (review needed)

  7. Structural removals — verify removed security functions were replaced

Cross-reference structural changes with git diff {before_ref}..{after_ref} to add source-level context to findings.

Severity classification:

SeverityStructural Signal
CRITICALNew tainted path to sensitive function, removed auth boundary
HIGHNew entrypoint + high blast radius, large CC increase on tainted node
MEDIUMNew trust-boundary-crossing edges, moderate CC increase
LOWAdded nodes without entrypoint reachability
INFODead code removal, complexity reductions

For detailed metric definitions, see references/evolution-metrics.md.

Phase 5: Clean Up

Remove git worktrees after the report is written:

git worktree remove "{before_dir}"
git worktree remove "{after_dir}"

Diff Reference

trailmark diff --json --language auto BEFORE AFTER
uv run {baseDir}/scripts/graph_diff.py [OPTIONS]

trailmark diff --language defaults to python. On a target in any other language that default still exits 0, emitting well-formed JSON with empty nodes, edges, and entrypoints arrays, so always pass the flag: auto detects and merges every supported language found under the target, and a single name (rust, solidity) or comma-separated list (python,rust) pins an explicit set. auto fails loudly with No supported languages detected under <path> when a snapshot holds nothing it can parse, which is the outcome you want. Confirm the language first; only then can an empty diff count as evidence that nothing changed.

Use trailmark diff for:

  • Node/edge changes

  • Added/removed/modified entrypoints

  • Human-readable structural diff reports

Use graph_diff.py for:

  • Subgraph membership changes derived from engine.preanalysis()

  • tainted, high_blast_radius, privilege_boundary, and related sets

ArgumentDefaultDescription
--beforerequiredPath to the "before" graph JSON
--afterrequiredPath to the "after" graph JSON
--indent2JSON output indentation

graph_diff.py input format: Trailmark JSON exports from engine.to_json(). graph_diff.py output: JSON structural diff for nodes, edges, and subgraphs.


Quality Checklist

Before delivering the report:

  • [ ] Both graphs built successfully (check summaries)

  • [ ] Pre-analysis ran on both snapshots

  • [ ] Native Trailmark diff computed (trailmark_diff.json); if it is empty, both snapshots' Phase 2 node counts were non-zero, so empty means stable

  • [ ] Subgraph diff computed and non-empty (subgraph_diff.json)

  • [ ] All subgraph changes interpreted (tainted, blast radius, etc.)

  • [ ] Critical findings include evidence (node IDs, edge diffs)

  • [ ] Severity levels assigned to all findings

  • [ ] Source-level context added via git diff cross-reference

  • [ ] Worktrees cleaned up (or temp dirs removed)

  • [ ] Report written to GRAPH_EVOLUTION_*.md


Integration

trailmark skill: Phase 2 uses the trailmark API for graph building and pre-analysis. All trailmark query patterns work on either snapshot's engine.

differential-review skill: Use graph-evolution for structural analysis, differential-review for line-level code review. The two are complementary — graph-evolution finds attack paths that text diffs miss, while differential-review provides git blame context and micro-adversarial analysis.

trailmark-review-gate skill: Use trailmark-review-gate after graph-evolution when a branch, pull request, fix commit, or release diff needs a PASS/WARN/FAIL/UNKNOWN structural review packet. The gate applies deterministic review rules to graph-evolution output; it does not replace human review.

genotoxic skill: If graph-evolution reveals new high-CC tainted nodes, feed them to genotoxic for mutation testing triage.

diagramming-code skill: Generate before/after diagrams to visualize structural changes. Use call-graph or data-flow diagrams focused on changed nodes.


Supporting Documentation

Files

references/evolution-metrics.md

Evolution Metrics Reference

This document explains each structural metric the graph-evolution skill tracks and why it matters for security analysis.

Contents

  • Node changes (added, removed, modified)

  • Edge changes (added, removed)

  • Complexity evolution

  • Attack surface changes

  • Blast radius shifts

  • Taint propagation changes

  • Privilege boundary changes


Node Changes

Added Nodes

New functions, methods, classes, or modules introduced between snapshots.

Security relevance:

  • New code has no review history and may lack test coverage

  • New public functions expand the attack surface

  • New classes may introduce state management complexity

Triage: Cross-reference added nodes against the after graph's entrypoints subgraph. Added nodes that are entrypoint-reachable get highest review priority.

Removed Nodes

Functions, methods, or classes deleted between snapshots.

Security relevance:

  • Removed validation functions may indicate weakened security controls

  • Removed error handlers can expose unhandled edge cases

  • Dead code removal is generally positive (reduces attack surface)

Triage: Check if removed nodes had privilege_boundary or taint_propagation annotations. If so, verify the security function was replaced, not just deleted.

Modified Nodes

Nodes present in both snapshots whose properties changed. Tracked properties:

PropertyWhat ChangedSecurity Concern
cyclomatic_complexityControl flow complexityHigher CC = more paths to test
parametersFunction signatureNew params may accept untrusted input
return_typeReturn type annotationType changes can break callers
line_spanLines of codeSignificant growth may indicate added logic

Edge Changes

Added Edges

New call relationships between functions.

Security relevance:

  • New calls from untrusted entrypoints to sensitive functions create attack paths that did not previously exist

  • New inherits or implements edges can change polymorphic dispatch

  • Cross-module calls may violate existing trust boundaries

Triage: For each added calls edge, check if source is in the tainted subgraph and target handles sensitive operations.

Removed Edges

Call relationships that no longer exist.

Security relevance:

  • Removed validation calls may mean input is no longer checked

  • Removed authorization calls can create privilege escalation

  • Usually benign during refactoring, but verify removed edges between security-relevant nodes


Complexity Evolution

Tracks per-node cyclomatic complexity changes between snapshots.

Thresholds:

CC DeltaSignificance
+1 to +3Minor — likely a new branch or error check
+4 to +9Moderate — new logic paths need test coverage
+10 or moreMajor — function is becoming difficult to reason about
NegativePositive — simplification usually reduces bug surface

Aggregate signals:

  • Mean CC increase across all modified nodes indicates codebase is growing more complex

  • Functions that crossed the CC > 10 threshold are new fuzzing candidates (per the genotoxic skill's criteria)


Attack Surface Changes

Derived from the entrypoints subgraph.

New entrypoints: Nodes that appear in the after entrypoints but not before. Each new entrypoint is a new way external input reaches the system.

Removed entrypoints: Nodes that were entrypoints in before but not after. Usually positive (reduced surface), but verify the functionality wasn't just moved.

Trust level changes: Compare entrypoint trust levels between snapshots. A function changing from trusted_internal to untrusted_external is a significant security event.


Blast Radius Shifts

Derived from the high_blast_radius subgraph (nodes with 10+ downstream dependents).

New high-blast nodes: Nodes that entered high_blast_radius in after. These now affect many downstream functions — bugs here have wide impact.

Reduced blast radius: Nodes that left high_blast_radius. Usually positive (decoupling), but verify the downstream functions weren't orphaned.


Taint Propagation Changes

Derived from the tainted subgraph (nodes reachable from untrusted entrypoints).

Newly tainted: Nodes that entered tainted in after. These can now be reached by untrusted input and must validate their inputs.

De-tainted: Nodes that left tainted. Usually means a trust boundary was added or an entrypoint was removed.

Critical combination: Nodes that are both newly tainted AND had their CC increase. These are the highest-priority review targets.


Privilege Boundary Changes

Derived from the privilege_boundary subgraph (edges where trust levels change).

New boundary crossings: Functions that appeared on a privilege boundary. These are points where trust transitions happen — common vulnerability locations.

Removed boundaries: Privilege boundaries that disappeared. Could mean trust was flattened (potentially unsafe) or that the boundary moved (needs verification).

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs a command-line tool, Needs Python, Needs Node.js

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Trail of Bits
Publisher
Trail of Bits (Official)
Source
trailofbits/skills · plugins/trailmark/skills/graph-evolution · commit e4dd0bb
Licence
Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Trail of Bits or Trail of Bits.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download graph-evolution.zip