Generated banner for Genotoxic
Generated logo for Genotoxic

Genotoxic

Trail of BitsOfficialPublished by Trail of Bits

  • Skill
  • Workflow
  • Software development
  • CC-BY-SA-4.0
  • No issues found

From the author

Graph-informed mutation testing triage. Parses codebases with Trailmark, runs mutation testing and necessist, then uses survived mutants, unnecessary test statements, and call graph data to identify false positives, missing test coverage, and fuzzing targets. Use when triaging survived mutants, analyzing mutation testing results, identifying test gaps, finding fuzzing targets from weak tests, running mutation frameworks (including circomvent and cairo-mutants), or using necessist.
— the description by Trail of Bits

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

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/trailofbits/genotoxic/download
  2. Unzip 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 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 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:

  1. QueryEngine.from_directory("{targetDir}", language="auto")

  2. Call engine.preanalysis() — mandatory before triage

  3. Export 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:

FieldDescription
File pathSource file containing the mutant
Line numberLine where mutation was applied
Mutation typeWhat was changed (operator, value, etc.)
Statussurvived, 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 --dump

Filter 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)
SignalBucketReasoning
No callers in graphFalse PositiveDead code, mutant is unreachable
Only test callersFalse PositiveTest infrastructure, not production
Logging/display stringFalse PositiveCosmetic, no behavioral impact
Equivalent mutantFalse PositiveBehavior unchanged despite mutation
Simple function, low CC, no entrypoint pathMissing TestsUnit test is straightforward
Error handling pathMissing TestsShould have negative test cases
Boundary condition (off-by-one)Missing TestsProperty-based test candidate
Pure function, deterministicMissing TestsEasy to test, high value
High CC (>10), entrypoint reachableFuzzing TargetComplex + exposed = fuzz it
Parser/validator/deserializerFuzzing TargetStructured input handling
Many callers (>10) + moderate CCFuzzing TargetHigh blast radius
Binary/wire protocol handlingFuzzing TargetFuzzers excel at format testing
Quick Classification (Necessist)
SignalBucketReasoning
Redundant setup or debug callFalse PositiveStatement genuinely unnecessary
Cannot map to production functionFalse PositiveNo graph context for triage
Call removed, no assertion checks its effectMissing TestsTest has weak assertions
Assertion removed, test still passesMissing TestsRedundant or insufficient coverage
Maps to high-CC entrypoint-reachable functionFuzzing TargetComplex + 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


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

references/graph-analysis.md

Graph Analysis for Mutant Triage

How to use trailmark's code graph data to contextualize survived mutants and assign them to the correct triage bucket.

Contents

  • Mapping mutants to graph nodes

  • Reachability analysis

  • Blast radius calculation

  • Complexity correlation

  • Annotation-driven triage

  • Batch triage workflow

  • Mapping necessist removals to graph nodes

  • Merging mutation and necessist results


Mapping Mutants to Graph Nodes

Each survived mutant has a file_path and line number. Map it to the containing function in the trailmark graph:

def find_containing_node(nodes: dict, file_path: str, line: int):
    """Find the graph node that contains a given source line."""
    candidates = []
    for node_id, node in nodes.items():
        loc = node.get("location", {})
        if not loc:
            continue
        if loc["file_path"] != file_path:
            continue
        if loc["start_line"] <= line <= loc["end_line"]:
            candidates.append((node_id, node))

    if not candidates:
        return None

    # Prefer the most specific (smallest range) containing node
    candidates.sort(
        key=lambda x: (
            x[1]["location"]["end_line"]
            - x[1]["location"]["start_line"]
        )
    )
    return candidates[0][1]

Why smallest range? A line inside a method is also inside its containing class. The method node is the more useful context for triage.


Reachability Analysis

Determine whether a mutated function is reachable from untrusted input.

From Entrypoints
def is_entrypoint_reachable(engine, node_id: str) -> bool:
    """Check if any entrypoint can reach this node."""
    return bool(engine.entrypoint_paths_to(node_id))
Entrypoint Path Details

For fuzzing targets, include the specific entrypoint paths in the report:

def entrypoint_paths(engine, node_id: str) -> list[dict]:
    """Get all entrypoint paths to this node with metadata."""
    surface_by_id = {
        ep["node_id"]: ep for ep in engine.attack_surface()
    }
    results = []
    for path in engine.entrypoint_paths_to(node_id):
        ep = surface_by_id.get(path[0], {})
        results.append({
            "entrypoint": path[0],
            "trust_level": ep.get("trust_level"),
            "kind": ep.get("kind"),
            "path": path,
            "hops": len(path),
        })
    return results
Trust Level Weighting

Not all entrypoints are equally dangerous:

Trust LevelWeightExamples
untrusted_external3xUser input, network data
semi_trusted_external2xPartner APIs, OAuth tokens
trusted_internal1xInternal service calls

Higher-weight entrypoints push mutants toward the fuzzing bucket.


Blast Radius Calculation

Blast radius measures how many other functions depend on the mutated function. Higher blast radius means a bug has wider impact.

Direct Callers
def blast_radius(engine, node_id: str) -> dict:
    """Calculate blast radius for a node."""
    callers = engine.callers_of(node_id)
    callees = engine.callees_of(node_id)

    return {
        "direct_callers": len(callers),
        "direct_callees": len(callees),
        "caller_ids": [c["id"] for c in callers],
    }
Transitive Impact

For critical functions, calculate transitive callers (all functions that eventually call this one):

def transitive_context(engine, node_id: str) -> dict:
    """Calculate transitive caller and entrypoint context."""
    ancestors = [
        node for node in engine.ancestors_of(node_id)
        if node["kind"] in {"function", "method"}
    ]
    paths = engine.entrypoint_paths_to(node_id)
    return {
        "transitive_callers": len(ancestors),
        "entrypoint_paths": len(paths),
        "entrypoint_reachable": bool(paths),
    }
Blast Radius Classification
Direct CallersTransitive CallersClassification
00Dead code (false positive)
1-51-10LOW
6-2011-50MEDIUM
21-5051-100HIGH
50+100+CRITICAL

Complexity Correlation

Cross-reference survived mutants with complexity data to distinguish "simple enough to unit test" from "complex enough to fuzz."

Per-Function Complexity
def complexity_context(engine, node_id: str) -> dict:
    """Get complexity context for triage decision."""
    hotspots = engine.complexity_hotspots(threshold=1)
    for h in hotspots:
        if h["id"] == node_id:
            return {
                "cyclomatic_complexity": h["cyclomatic_complexity"],
                "is_hotspot": h["cyclomatic_complexity"] >= 10,
            }
    return {"cyclomatic_complexity": 0, "is_hotspot": False}
Decision Matrix
CCEntrypoint ReachableBlast RadiusBucket
<5NoAnyMissing Tests
<5YesLOWMissing Tests
<5YesHIGH+Missing Tests (priority)
5-10NoLOWMissing Tests
5-10NoHIGH+Missing Tests (priority)
5-10YesAnyFuzzing Target
>10AnyAnyFuzzing Target

Annotation-Driven Triage

Use trailmark annotations to record triage decisions and refine classification over time.

Recording Decisions
from trailmark.models import AnnotationKind

# Mark a function as triaged
engine.annotate(
    node_id,
    AnnotationKind.ASSUMPTION,
    "genotoxic: false_positive (equivalent mutant in logging)",
    source="llm",
)

# Mark a fuzzing target with rationale
engine.annotate(
    node_id,
    AnnotationKind.ASSUMPTION,
    "genotoxic: fuzzing_target (CC=14, entrypoint-reachable via /api/parse)",
    source="llm",
)
Querying Previous Triage
# Check if a function was previously triaged
annotations = engine.annotations_of(node_id)
genotoxic_annotations = [
    a for a in annotations
    if a["description"].startswith("genotoxic:")
]

This enables incremental triage across multiple mutation testing runs.


Batch Triage Workflow

For large codebases with many survived mutants, process in batch:

import json

def batch_triage(engine, survived_mutants: list[dict]) -> dict:
    """Classify all survived mutants."""
    graph_json = json.loads(engine.to_json())
    nodes = graph_json["nodes"]

    results = {
        "false_positives": [],
        "missing_tests": [],
        "fuzzing_targets": [],
    }

    for mutant in survived_mutants:
        node = find_containing_node(
            nodes, mutant["file_path"], mutant["line"]
        )
        if not node:
            results["false_positives"].append({
                **mutant,
                "reason": "no containing function in graph",
            })
            continue

        node_id = node["id"]
        callers = engine.callers_of(node_id)
        cc = node.get("cyclomatic_complexity", 0) or 0

        # Dead code
        if not callers:
            results["false_positives"].append({
                **mutant,
                "reason": "no callers (dead code)",
                "node_id": node_id,
            })
            continue

        reachable = is_entrypoint_reachable(engine, node_id)

        # Fuzzing criteria
        if (cc > 10 and reachable) or (len(callers) > 10 and cc > 5):
            ep_paths = entrypoint_paths(engine, node_id)
            results["fuzzing_targets"].append({
                **mutant,
                "node_id": node_id,
                "cyclomatic_complexity": cc,
                "caller_count": len(callers),
                "entrypoint_paths": ep_paths,
                "blast_radius": blast_radius(engine, node_id),
            })
            continue

        # Default: missing tests
        results["missing_tests"].append({
            **mutant,
            "node_id": node_id,
            "cyclomatic_complexity": cc,
            "caller_count": len(callers),
            "entrypoint_reachable": reachable,
        })

    return results
Performance Considerations
  • Path queries are expensive. Cache paths_between results when checking multiple mutants against the same entrypoints.

  • Process by function, not by mutant. Multiple mutants in the same function share the same graph context. Group mutants by containing function first, query graph once per function.

  • Use complexity_hotspots as a prefilter. Functions with CC < 5 are almost never fuzzing targets. Skip reachability analysis for them unless caller count is very high.


Mapping Necessist Removals to Graph Nodes

Necessist findings reference test code locations, but triage requires the production function that the removed statement exercises. Extract the called function name from the removed statement and match it against graph nodes.

import re


def map_removal_to_production_node(
    nodes: dict,
    removed_statement: str,
    test_file_path: str,
) -> dict | None:
    """Map a necessist removal to the production function it exercises."""
    # Extract function/method name from the removed statement.
    # Handles: obj.method(args), function(args), obj.method!(args)
    match = re.search(
        r"(?:(\w+)\.)?(\w+!?)\s*\(", removed_statement
    )
    if not match:
        return None

    func_name = match.group(2)

    # Search graph nodes for matching function name
    candidates = [
        (nid, n) for nid, n in nodes.items()
        if n.get("name") == func_name
        and "test" not in n.get("location", {})
            .get("file_path", "").lower()
    ]

    if len(candidates) == 1:
        return candidates[0][1]

    # Disambiguate: prefer node in the production module
    # that mirrors the test file path
    prod_path = infer_production_path(test_file_path)
    for nid, n in candidates:
        if n.get("location", {}).get("file_path") == prod_path:
            return n

    # Fall back to first non-test candidate
    return candidates[0][1] if candidates else None


def infer_production_path(test_file_path: str) -> str:
    """Heuristic: map test file to likely production file.

    tests/test_parser.py  → src/parser.py
    test/parser_test.go   → parser.go
    tests/Parser.test.ts  → src/Parser.ts
    """
    path = test_file_path
    # Strip test directory prefixes
    path = re.sub(r"^tests?/", "src/", path)
    # Strip test_ prefix or _test / .test suffix
    path = re.sub(r"test_(\w+)", r"\1", path)
    path = re.sub(r"(\w+)_test\.", r"\1.", path)
    path = re.sub(r"(\w+)\.test\.", r"\1.", path)
    return path

When mapping fails: If no production node matches, classify the removal as a false positive with reason "unmappable to production code." This is conservative — the removal may still be meaningful, but without graph context triage cannot assign a confident bucket.


Merging Mutation and Necessist Results

When both mutation testing and necessist produce findings for the same production function, this is a corroborated finding: the function has both uncaught production mutations and unnecessary test statements. Corroborated findings are highest confidence.

def merge_results(
    mutation_results: dict,
    necessist_results: dict,
) -> dict:
    """Merge mutation and necessist triage results.

    Identifies corroborated findings where both tools flag
    the same production function.
    """
    merged = {
        "corroborated": [],
        "false_positives": (
            mutation_results["false_positives"]
            + necessist_results["false_positives"]
        ),
        "missing_tests": [],
        "fuzzing_targets": [],
    }

    # Index necessist findings by production node_id
    necessist_by_node = {}
    for item in (
        necessist_results["missing_tests"]
        + necessist_results["fuzzing_targets"]
    ):
        nid = item.get("node_id")
        if nid:
            necessist_by_node.setdefault(nid, []).append(item)

    # Check mutation findings for corroboration
    for bucket in ("missing_tests", "fuzzing_targets"):
        for item in mutation_results[bucket]:
            nid = item.get("node_id")
            if nid and nid in necessist_by_node:
                merged["corroborated"].append({
                    "node_id": nid,
                    "mutation": item,
                    "necessist": necessist_by_node.pop(nid),
                })
            else:
                merged[bucket].append(item)

    # Add remaining non-corroborated necessist findings
    for items in necessist_by_node.values():
        for item in items:
            bucket = (
                "fuzzing_targets"
                if item in necessist_results["fuzzing_targets"]
                else "missing_tests"
            )
            merged[bucket].append(item)

    return merged

Corroborated findings should appear in a dedicated report section before the individual buckets, since they represent the highest-value action items.

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 or Trail of Bits.

Download ZIP

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