Genotoxic
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Workflow
- Software development
- CC-BY-SA-4.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Genotoxic streamlines the triage of mutation testing results by integrating code graph data from Trailmark with mutation testing frameworks and Necessist. It transforms raw lists of survived mutants into actionable categories—false positives, missing unit tests, and high-priority fuzzing targets—by analyzing code complexity, reachability, and blast radius. This approach reduces noise in mutation testing and ensures that test improvements are focused on the most critical and vulnerable parts of the codebase.
What it does
- Parses codebases to build a structural graph of functions and entrypoints.
- Executes mutation testing frameworks to identify code logic that lacks sufficient test coverage.
- Runs Necessist to detect weak test assertions and redundant test statements.
- Maps survived mutants and test removals to specific production code nodes.
- Classifies findings into false positives, missing tests, or fuzzing targets based on graph context.
- Generates a structured triage report for prioritized remediation.
When to use it
- After running mutation testing and receiving a large, unmanageable list of survived mutants.
- When you need to prioritize which parts of your codebase require new unit tests.
- When identifying complex, entrypoint-reachable functions that are better suited for fuzzing.
- When you want to filter out harmless or equivalent mutants from your test results.
- When evaluating the effectiveness of existing test assertions using Necessist.
Advantages
- Reduces manual triage effort by using automated graph analysis to filter noise.
- Provides data-driven justification for prioritizing specific test improvements.
- Identifies high-value fuzzing targets that unit tests might miss.
- Corroborates findings across multiple testing tools for higher confidence.
- Integrates seamlessly with existing mutation testing and analysis toolchains.
Usage examples
- “I have a list of 50 survived mutants from my Python project. How do I triage them?”
- The agent will guide you to build a Trailmark graph, run the mutation analysis, and use the Phase 3 triage workflow to categorize the mutants into false positives, missing tests, or fuzzing targets.
- “How can I tell if a survived mutant is actually a high-priority security risk?”
- The agent will use the graph data to check if the mutated function is reachable from an untrusted entrypoint and calculate its blast radius, flagging it as a fuzzing target if it meets the criteria.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/trailofbits/genotoxic/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip genotoxic.zip -d .agents/skills
Documentation by Trail of Bits
Genotoxic
Combines mutation testing and necessist (test statement removal) with code graph analysis to triage findings into actionable categories: false positives, missing unit tests, and fuzzing targets.
When to Use
After mutation testing reveals survived mutants that need triage
Identifying where unit tests would have the highest impact
Finding functions that need fuzz harnesses instead of unit tests
Prioritizing test improvements using data flow context
Filtering out harmless mutants from actionable ones
Finding unnecessary test statements that indicate weak assertions (necessist)
When NOT to Use
Codebase has no existing test suite (write tests first)
Pure documentation or configuration changes
Single-file scripts with trivial logic
Prerequisites
trailmark installed — if
uv run trailmarkfails, run:uv tool install trailmark
Python snippets: uv run --with trailmark python - (a tool env is not importable)
**DO NOT** fall back to "manual verification" or "manual analysis"
as a substitute for running trailmark. Install it first. If installation
fails, report the error instead of switching to manual analysis.
- A **mutation testing framework** for the target language — if the framework
command fails (not found, not installed), install it using the instructions
in [references/mutation-frameworks.md](references/mutation-frameworks.md).
**DO NOT** fall back to "manual mutation analysis" or skip mutation testing.
Install the framework first. If installation fails, report the error
instead of switching to manual mutation analysis.
- **necessist** (optional, recommended) — if the target language is
supported (Go, Rust, Solidity/Foundry, TypeScript/Hardhat,
TypeScript/Vitest, Rust/Anchor), install with `cargo install necessist`.
See [references/mutation-frameworks.md](references/mutation-frameworks.md)
for details.
- An existing test suite that passes
- **macOS environment**: Run `ulimit -n 1024` before any `mull-runner`
invocation. macOS Tahoe (26+) sets unlimited file descriptors by
default, which crashes Mull's subprocess spawning. See
[references/mutation-frameworks.md](references/mutation-frameworks.md)
for details.
---
## Rationalizations to Reject
| Rationalization | Why It's Wrong | Required Action |
|-----------------|----------------|-----------------|
| "All survived mutants need tests" | Many are harmless or equivalent | Triage before writing tests |
| "Mutation testing is too noisy" | Noise means you're not triaging | Use graph data to filter |
| "Unit tests cover everything" | Complex data flows need fuzzing | Check entrypoint reachability |
| "Dead code mutants don't matter" | Dead code should be removed | Flag for cleanup |
| "Low complexity = low risk" | Boundary bugs hide in simple code | Check mutant location |
| "Tool isn't installed, I'll do it manually" | Manual analysis misses what tooling catches | Install the tool first |
| "Necessist isn't mutation testing, skip it" | Necessist finds what mutation testing misses: weak tests | Run both when the language supports it |
---
## Quick Start
```bash
# 1. Build the code graph
uv run trailmark analyze --language auto --summary {targetDir}
# 2. Run mutation testing (language-dependent)
# Python:
uv run mutmut run --paths-to-mutate {targetDir}/src
uv run mutmut results
# 2b. Run necessist (if language supported)
necessist
# 3. Analyze results with this skill's workflow (Phase 3)Workflow Overview
Phase 1: Graph Build → Parse codebase with trailmark
↓
Phase 2: Mutation Run → Execute mutation testing framework
Phase 2b: Necessist Run → Remove test statements (optional, parallel)
↓
Phase 3: Triage → Classify findings using graph data
↓
Output: Categorized Report
├── Corroborated (both tools flag same function — highest value)
├── False Positives (harmless, skip)
├── Missing Tests (write unit tests)
└── Fuzzing Targets (set up fuzz harnesses)Decision Tree
├─ Need to set up mutation testing for a language?
│ └─ Read: references/mutation-frameworks.md
│
├─ Need to set up necessist or find weak test statements?
│ └─ Read: references/mutation-frameworks.md (Necessist section)
│
├─ Need to understand the triage criteria in depth?
│ └─ Read: references/triage-methodology.md
│
├─ Need to understand how graph data informs triage?
│ └─ Read: references/graph-analysis.md
│
└─ Already have results + graph? Use Phase 3 below.Phase 1: Build Code Graph and Run Pre-Analysis
Parse the target codebase with trailmark and run pre-analysis before mutation testing. Pre-analysis computes blast radius, entry points, privilege boundaries, and taint propagation, which Phase 3 uses for triage.
uv run trailmark analyze --language auto --summary {targetDir}Use the QueryEngine API to build the graph and run pre-analysis:
QueryEngine.from_directory("{targetDir}", language="auto")Call
engine.preanalysis()— mandatory before triageExport with
engine.to_json()for cross-referencing with mutation results
If auto-detection is wrong for the target, rerun with an explicit language or
comma-separated list such as python,rust.
See references/graph-analysis.md for the full API: node mapping, reachability queries, blast radius, and pre-analysis subgraph lookups.
Phase 2: Run Mutation Testing
Select and run the appropriate framework. See references/mutation-frameworks.md for language-specific setup.
Capture survived mutants. Each framework reports differently, but extract these fields per mutant:
| Field | Description |
|---|---|
| File path | Source file containing the mutant |
| Line number | Line where mutation was applied |
| Mutation type | What was changed (operator, value, etc.) |
| Status | survived, killed, timeout, error |
Filter to survived mutants only for Phase 3.
Phase 2b: Run Necessist (Optional)
If the target language is supported (Go, Rust, Solidity/Foundry, TypeScript/Hardhat, TypeScript/Vitest, Rust/Anchor), run necessist to find unnecessary test statements. This runs independently of Phase 2 and can execute in parallel.
# Auto-detect framework
necessist
# Or target specific test files
necessist tests/test_parser.rs
# Export results
necessist --dumpFilter to findings where the test passed after removal. See references/mutation-frameworks.md for framework-specific configuration and the normalized record format.
Map each removal to a production function using the algorithm in references/graph-analysis.md.
Phase 3: Triage Findings
For each survived mutant and each necessist removal, determine its triage bucket using graph data. Necessist removals must first be mapped to a production function (see references/graph-analysis.md).
Quick Classification (Mutation Testing)
| Signal | Bucket | Reasoning |
|---|---|---|
| No callers in graph | False Positive | Dead code, mutant is unreachable |
| Only test callers | False Positive | Test infrastructure, not production |
| Logging/display string | False Positive | Cosmetic, no behavioral impact |
| Equivalent mutant | False Positive | Behavior unchanged despite mutation |
| Simple function, low CC, no entrypoint path | Missing Tests | Unit test is straightforward |
| Error handling path | Missing Tests | Should have negative test cases |
| Boundary condition (off-by-one) | Missing Tests | Property-based test candidate |
| Pure function, deterministic | Missing Tests | Easy to test, high value |
| High CC (>10), entrypoint reachable | Fuzzing Target | Complex + exposed = fuzz it |
| Parser/validator/deserializer | Fuzzing Target | Structured input handling |
| Many callers (>10) + moderate CC | Fuzzing Target | High blast radius |
| Binary/wire protocol handling | Fuzzing Target | Fuzzers excel at format testing |
Quick Classification (Necessist)
| Signal | Bucket | Reasoning |
|---|---|---|
| Redundant setup or debug call | False Positive | Statement genuinely unnecessary |
| Cannot map to production function | False Positive | No graph context for triage |
| Call removed, no assertion checks its effect | Missing Tests | Test has weak assertions |
| Assertion removed, test still passes | Missing Tests | Redundant or insufficient coverage |
| Maps to high-CC entrypoint-reachable function | Fuzzing Target | Complex + exposed + weak test |
When both mutation testing and necessist flag the same production function, mark as corroborated — highest confidence finding.
For detailed criteria, see references/triage-methodology.md.
Graph Queries for Triage
For each mutant, map it to its containing graph node and use pre-analysis subgraphs (tainted, high_blast_radius, privilege_boundary) from Phase 1 to classify it. The classification logic checks: no callers → false positive, privilege boundary → fuzzing, high CC + tainted → fuzzing, high blast radius → fuzzing, otherwise → missing tests.
See references/graph-analysis.md for
the batch_triage implementation and node mapping functions.
Output Format
Generate a markdown report:
# Genotoxic Triage Report
## Summary
- Total survived mutants: N
- Total necessist removals: N
- Corroborated findings: N
- False positives: N (N%)
- Missing test coverage: N (N%)
- Fuzzing targets: N (N%)
## Corroborated Findings
| File | Line | Function | Mutation Signal | Necessist Signal | Action |
|------|------|----------|----------------|------------------|--------|
## False Positives
| File | Line | Mutation | Reason | Source |
|------|------|----------|--------|--------|
## Missing Test Coverage
| File | Line | Function | CC | Callers | Suggested Test | Source |
|------|------|----------|----|---------|----------------|--------|
## Fuzzing Targets
| File | Line | Function | CC | Entrypoint Path | Blast Radius | Source |
|------|------|----------|----|-----------------|--------------|--------|The Source column is mutation, necessist, or corroborated.
Write the report to GENOTOXIC_REPORT.md in the working directory.
Quality Checklist
Before delivering:
[ ] Trailmark graph built for target language
[ ] Mutation framework ran to completion
[ ] Necessist ran (if language supported) or noted as not applicable
[ ] All survived mutants triaged (none unclassified)
[ ] All necessist removals triaged (if applicable)
[ ] Corroborated findings identified (if both tools ran)
[ ] False positives have clear justifications
[ ] Missing test items include suggested test type
[ ] Fuzzing targets include entrypoint paths and blast radius
[ ] Report file written to
GENOTOXIC_REPORT.md[ ] User notified with summary statistics
Integration
trailmark skill:
Phase 1: Build code graph, query complexity and entrypoints
Phase 3: Caller analysis, reachability, blast radius
property-based-testing skill:
Missing test coverage items involving boundary conditions
Roundtrip/idempotence properties for serialization mutants
testing-handbook-skills (fuzzing):
Fuzzing target items: use
harness-writing,cargo-fuzz,atheris
Supporting Documentation
references/mutation-frameworks.md - Language-specific framework setup, output parsing, and necessist configuration
references/triage-methodology.md - Detailed triage criteria, edge cases, and worked examples for both mutation testing and necessist
references/graph-analysis.md - Graph query patterns, test-to-production mapping, and result merging
First-time users: Start with Phase 1 (graph build), then run mutations, then use the Quick Classification table in Phase 3.
Experienced users: Jump to Phase 3 and use the Decision Tree to load specific reference material.
Files
- ATTRIBUTION.mdAdded by AutoGineer921 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md12.7 KiB
- agents/openai.yaml229 B
- assets/trail-of-bits-mark.svg3.0 KiB
- references/graph-analysis.md12.8 KiB
- references/mutation-frameworks.md28.1 KiB
- references/triage-methodology.md11.8 KiB
references/triage-methodology.md
Triage Methodology
Detailed criteria for classifying survived mutants into actionable buckets.
Contents
False positive detection
Missing test coverage identification
Fuzzing target selection
Edge cases and ambiguous mutants
Worked examples
Necessist removal triage
False Positive Detection
A mutant is a false positive when killing it would not improve code quality or catch real bugs. Classify as false positive when ANY of these conditions hold:
Dead Code
The mutated function has zero callers in the trailmark graph.
callers = engine.callers_of(node_id)
if not callers:
# Dead code. The mutant is unreachable in production.
# Action: flag function for removal, not testing.Subtlety: A function with no direct callers may still be reachable
via dynamic dispatch (reflection, callbacks, decorators). Check edge
confidence: if all edges to the function are uncertain, investigate
before dismissing.
Test-Only Code
The function is only called from test files.
callers = engine.callers_of(node_id)
prod_callers = [
c for c in callers
if "test" not in c["location"]["file_path"].lower()
]
if not prod_callers:
# Only tests call this. Mutant in test infrastructure.Equivalent Mutants
The mutation produces identical behavior. Common patterns:
| Mutation | Why Equivalent |
|---|---|
x > 0 → x >= 1 | Identical for integers |
x != 0 → x > 0 | Identical for unsigned types |
return x → return +x | Unary plus is a no-op |
| String literal change in log message | No behavioral impact |
| Reorder of commutative operations | a + b == b + a |
Detection strategy: Check if the mutation is in a logging call, display string, comment-adjacent code, or assertion message. These are cosmetic and do not affect program behavior.
Redundant Checks
The mutation weakens a condition, but another check in the same call path enforces the same constraint:
# Caller validates x > 0 before calling this function.
# Mutating this function's own x > 0 check is redundant.
callers = engine.callers_of(node_id)
# Inspect caller source for equivalent preconditions.Use trailmark annotations to track this:
from trailmark.models import AnnotationKind
engine.annotate(
node_id,
AnnotationKind.PRECONDITION,
"x > 0 enforced by all callers",
source="llm",
)Missing Test Coverage
A mutant indicates missing test coverage when:
The function is reachable in production (has callers)
The mutated behavior should be caught by tests
A unit test is the appropriate testing strategy
Criteria
| Signal | Why Unit Test | Priority |
|---|---|---|
| Pure function, no side effects | Deterministic, easy to test | HIGH |
| Low CC (<5) | Few paths to cover | HIGH |
| Error/exception handling path | Negative tests needed | HIGH |
| Boundary condition (off-by-one) | Property-based test | MEDIUM |
| Return value mutation | Assert on return values | MEDIUM |
| State transition logic | State machine tests | MEDIUM |
| Configuration/flag handling | Parameter variation tests | LOW |
Suggested Test Types
Map the mutation type to a test strategy:
| Mutation Type | Suggested Test |
|---|---|
Arithmetic operator (+ → -) | Value assertion on known inputs |
Comparison operator (< → <=) | Boundary value test |
Boolean negation (True → False) | Branch coverage test |
Return value (return x → return None) | Return value assertion |
| Removed statement | Side effect verification |
| Exception removal | Negative test (expect failure) |
When to Prefer Property-Based Testing
If the survived mutant involves:
Serialization/deserialization (roundtrip property)
Idempotent operations (applying twice = applying once)
Ordering invariants (sorted output)
Numeric ranges or bounds
Use the property-based-testing skill for guidance.
Fuzzing Target Selection
A survived mutant is a fuzzing target when unit testing alone is insufficient due to complexity, input space, or exposure to untrusted data.
Criteria
| Signal | Threshold | Why Fuzzing |
|---|---|---|
| Cyclomatic complexity | CC > 10 | Too many paths for manual tests |
| Entrypoint reachable | Any path from untrusted input | Attacker-controlled data |
| Caller count | > 10 callers | High blast radius |
| Input parsing | Handles structured data | Fuzzers generate diverse inputs |
| Binary/wire protocol | Processes byte sequences | Coverage-guided exploration |
| Recursive logic | Processes nested structures | Depth/stack exhaustion |
| State machine | Multiple state transitions | State space exploration |
Prioritization
Combine signals for priority assignment:
CRITICAL: Entrypoint reachable + CC > 15 + parser/validator
HIGH: Entrypoint reachable + CC > 10
HIGH: CC > 10 + caller count > 20
MEDIUM: CC > 10 OR (entrypoint reachable + caller count > 10)
LOW: Moderate complexity, not entrypoint reachableFramework Selection
Based on target language, recommend the appropriate fuzzer:
| Language | Fuzzer | Skill Reference |
|---|---|---|
| Python | Atheris | testing-handbook-skills:atheris |
| Rust | cargo-fuzz | testing-handbook-skills:cargo-fuzz |
| C/C++ | libFuzzer or AFL++ | testing-handbook-skills:libfuzzer |
| Go | go-fuzz (native) | Built-in go test -fuzz |
| Ruby | Ruzzy | testing-handbook-skills:ruzzy |
| Java | Jazzer | JUnit integration |
| JavaScript | jsfuzz | npm package |
Edge Cases and Ambiguous Mutants
Some mutants don't cleanly fit one bucket. Resolution rules:
Mutant in Validation Code
If the mutant weakens input validation:
Entrypoint reachable? → Fuzzing target (attacker can exploit)
Internal only? → Missing test (regression risk)
Mutant in Error Path
If the mutant changes error handling behavior:
Error path tested? → Check if test expects specific error
Error path untested? → Missing test (negative test case)
Error in parser? → Fuzzing target (malformed input testing)
Mutant Straddles Complexity Threshold
CC is near the threshold (8-12 range):
Has entrypoint path? → Fuzzing target (exposure wins)
No entrypoint path? → Missing test (unit test is feasible)
Tie-Breaking Rule
When signals conflict, prefer the higher-assurance category:
Fuzzing Target > Missing Test > False PositiveA function that could be unit tested but is also entrypoint-reachable and complex should be fuzzed. Fuzzing subsumes the unit test goal while providing broader coverage.
Worked Example
Scenario: Python web application, mutmut reports 47 survived mutants.
Graph context (from trailmark):
312 nodes, 1,847 edges
8 entrypoints (Flask route handlers)
14 functions with CC > 10
Triage results:
| Category | Count | Examples |
|---|---|---|
| False Positive | 12 | 5 logging strings, 3 dead utils, 4 equivalent |
| Missing Tests | 23 | 8 error paths, 7 return values, 5 boundary, 3 config |
| Fuzzing Targets | 12 | 4 request parsers, 3 validators, 3 query builders, 2 serializers |
Key decisions:
parse_query_params(CC=14, entrypoint-reachable via/search) → Fuzzingformat_error_response(CC=3, 2 callers, string formatting) → False positive (cosmetic)validate_email(CC=6, 4 callers, no entrypoint path) → Missing test (boundary cases)build_sql_filter(CC=12, entrypoint-reachable via/api/filter) → Fuzzing (injection risk)
Necessist Removal Triage
Necessist findings differ from mutation testing: they identify test statements whose removal doesn't cause test failure. Triage maps each removal to a production function using the graph analysis algorithm and then classifies it.
False Positive Detection (Necessist)
Classify a necessist removal as false positive when:
| Signal | Reason |
|---|---|
| Redundant setup | Same call made elsewhere in the test or fixture |
| Debug/logging call | println, console.log, dbg! in test code |
| Teardown/cleanup | Removal of resource cleanup that doesn't affect assertions |
| Dead production code | Production function has no callers in graph |
| Unmappable statement | Cannot identify which production function is exercised |
Missing Test Coverage (Necessist)
A removal indicates missing coverage when the test should fail but doesn't — meaning the test has weak or missing assertions:
| Signal | Action |
|---|---|
| Function call removed, no assertion checks its effect | Add assertion on the function's return value or side effect |
| Assertion removed, remaining assertions still pass | The removed assertion covered unique behavior — restore and strengthen |
| Setup step removed with no downstream impact | Setup should affect test outcome; add assertions that depend on it |
| State mutation removed, test still passes | Test doesn't verify state changes — add state assertions |
Fuzzing Target Selection (Necessist)
After mapping to a production function, apply the same graph-based criteria as mutation testing:
CC > 10 and entrypoint reachable → Fuzzing Target
High blast radius and CC > 5 → Fuzzing Target
On a privilege boundary → Fuzzing Target
The reasoning is identical: if a production function is complex, exposed, and its test coverage is demonstrably weak (necessist proved a test statement was unnecessary), fuzzing is the appropriate response.
Edge Cases (Necessist)
Async/await removals: Removing an await may cause a test to pass
because the assertion runs before the async operation completes. This
is a genuine test weakness (race condition in test), not a false positive.
Classify as missing test coverage — the test needs to properly await
and assert.
Macro expansions (Foundry/Anchor): Cheatcodes like vm.prank,
vm.expectRevert, vm.warp are setup-critical. If removing one causes
the test to still pass, the test likely doesn't exercise the behavior
the cheatcode was supposed to enable. Classify as missing test coverage
unless the cheatcode is purely cosmetic (vm.label).
Chained method calls: foo.bar().baz() — necessist may remove
the entire chain. Map to the outermost call (foo.bar) for triage.
If the chain involves multiple production functions, triage against
the one with highest blast radius.
Solidity assert vs require: Removing a require check in a
test helper is different from removing an assert in a test body.
require removals in test helpers are usually false positives (guard
conditions). assert removals in test bodies are missing coverage.
Worked Example: Foundry Project
Scenario: Foundry DeFi lending protocol, necessist reports 31 removals that passed.
Graph context (from trailmark):
89 nodes, 412 edges
5 entrypoints (external functions)
6 functions with CC > 10
Triage results:
| Category | Count | Examples |
|---|---|---|
| False Positive | 8 | 3 vm.label calls, 2 console.log, 3 redundant vm.deal |
| Missing Tests | 16 | 5 missing return value checks, 4 state assertions, 4 event assertions, 3 removed assertEq with redundant coverage |
| Fuzzing Targets | 7 | 3 liquidation path functions, 2 interest calculation, 2 oracle price handling |
Key decisions:
calculateInterest(CC=11, reachable viaborrow()) → Fuzzing — test removedassertApproxEqReland still passed, meaning the interest calculation has untested edge cases in a complex, exposed functionvm.label(address(pool), "pool")→ False positive — cosmetic labeling for trace outputassertEq(token.balanceOf(user), expectedBalance)removed and test passes → Missing test — the balance check was the only assertion verifying the transfer succeeded
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 Python, Needs Node.js
Security
No issues found by our static scan· No issues found
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/genotoxic · 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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download genotoxic.zip