Claude Skill

variant-analysis

Hunts for the other instances of a bug already found — the variants of one root cause across a codebase. Use immediately after a vulnerability, logic bug, or bad pattern turns up in a specific file and the question becomes where else it occurs, including the bare conversational f

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download trailofbits-skills-plugins_variant-analysis_skills_variant-analysis-123037e.zip · 20 KB
trailofbits/skills 7234 616 forks CC-BY-SA-4.0 Updated 9h ago
Part of trailofbits/skills — 100 skills

Install

skills CLI npx skills add https://github.com/trailofbits/skills/tree/main/plugins/variant-analysis/skills/variant-analysis
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install trailofbits-skills@llmmart
Git git clone https://github.com/trailofbits/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole trailofbits/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Variant Analysis

Find the other instances of a bug you have already found. One root cause usually has several manifestations, and they are rarely in the module where you found the first one.

When to Use

  • A vulnerability has been found and you need to search for similar instances
  • Building or refining CodeQL/Semgrep queries for security patterns
  • Performing systematic code audits after an initial issue discovery
  • Analyzing how a single root cause manifests in different code paths

When NOT to Use

  • Initial vulnerability discovery — use audit-context-building or a domain-specific audit
  • General code review with no known pattern to search for
  • Writing fix recommendations — use issue-writer
  • Understanding unfamiliar code — use audit-context-building first

The Five Steps

Read the reference for a step when you reach it.

1. Understand the original issue. Extract the root cause — why the code is wrong, not what it does — and enumerate the directions a variant could hide in: related identifiers, other manifestations of the same mistake, data-type edge cases. → references/root-cause.md

2. Create an exact match. Write a pattern matching ONLY the known instance and confirm it hits. A pattern that matches nothing means you have misunderstood the bug, and every search built on it is calibrated against the wrong code.

3–4. Generalize one element at a time. Climb from the exact match toward the pattern family, running and reading all matches after each single change. Stop when more than half the matches are noise. → references/searching.md — abstraction ladder, tool selection, false-positive filters

5. Triage. Decide which candidates are real, and say so with a severity attached. → references/triage.md

Then write it up, including the patterns that failed and a CI rule to prevent regression. → references/reporting.md

Running it as a Workflow

This plugin ships /variant-analysis:variants, which runs the five steps across parallel subagents — one per expansion axis, looping until the sweep stops finding anything new. Each stage reads the reference above that matches its job.

Use the workflow when the codebase is large or the root cause has many manifestations. Work the steps directly when the search is narrow or you want a say in each generalization.

What Makes Hunts Fail

  1. Narrow scope — searching only the module the original bug was in
  2. Pattern too specific — searching one attribute and missing the family around it
  3. One vulnerability class — chasing a single manifestation of the root cause
  4. Happy-path testing — never trying the null, empty, and boundary cases
  5. Generalizing too fast — abstracting several elements at once, so noise cannot be attributed to any one of them

The first three are covered in root-cause.md and searching.md, the fourth in triage.md.

Resources

CodeQL (resources/codeql/): python.ql, javascript.ql, java.ql, go.ql, cpp.ql

Semgrep (resources/semgrep/): python.yaml, javascript.yaml, java.yaml, go.yaml, cpp.yaml

Report: resources/variant-report-template.md

Files (skills)
  • agents
    • openai.yaml 247 B
      interface:
        display_name: "Variant Analysis"
        short_description: "Find other instances of a known vulnerability or bug pattern"
        icon_small: "assets/trail-of-bits-mark.svg"
        icon_large: "assets/trail-of-bits-mark.svg"
        brand_color: "#D83A34"
      
  • assets
    • trail-of-bits-mark.svg 3 KB · in bundle
  • references
    • reporting.md 1.5 KB
      # Reporting a Variant Hunt
      
      Advice for the final stage. The report is written for a security engineer that is reviewing vulnerability variants.
      
      ## Structure
      
      Fill `../resources/variant-report-template.md` — the path is relative to this file, so
      from the skill root it is `resources/variant-report-template.md`. Every section earns its
      place:
      
      - **Summary** — original bug, date, codebase, count
      - **Original vulnerability** — the root cause statement, the origin location, the code
      - **Search methodology** — the patterns tried, at which level, with match and FP counts
      - **Findings** — one block per confirmed variant, severity-ordered
      - **False positive patterns** — grouped by reason, not one row per match
      - **Recommendations** — immediate fixes, then preventive measures
      
      ## The Methodology Table
      
      | Version | Pattern | Tool | Matches | TP | FP |
      |---------|---------|------|---------|----|----|
      | v1 | exact | ripgrep | 1 | 1 | 0 |
      | v2 | abstract | semgrep | N | N | N |
      
      This table makes the hunt reproducible. It records which abstractions worked, which
      produced noise, and where the search stopped.
      
      Record the patterns that failed alongside those that worked.
      
      ## Quote the Real Code
      
      Read each confirmed location and quote what is actually there. A report that paraphrases
      code loses the detail a reviewer needs to confirm the finding, and a wrong quote destroys
      trust in every other finding in the document.
      
      ## Leave a Regression Guard
      
      End with a CI-ready rule derived from whichever pattern found the most variants.
      
    • root-cause.md 4.1 KB
      # Root Cause and Expansion Axes
      
      Strategy for the first stage of a variant hunt: turning one known bug into the set of
      independent directions worth searching. Everything downstream is calibrated against what
      you produce here, so a shallow root cause caps the quality of the entire hunt.
      
      ## Why Variants Exist
      
      Vulnerabilities cluster because developers make consistent mistakes:
      
      1. **Developer habits**: the same person writes similar code and makes similar errors
      2. **Copy-paste propagation**: boilerplate spreads a bug across the codebase
      3. **API misuse patterns**: complex APIs invite consistent misunderstandings
      4. **Framework idioms**: framework patterns create predictable vulnerability shapes
      5. **Incomplete fixes**: the original bug was fixed in one place and missed elsewhere
      
      Understanding WHY a variant exists predicts WHERE to find it. A copy-paste bug clusters in
      sibling files; an API misuse bug clusters at every call site of that API, anywhere.
      
      ## Extracting the Root Cause
      
      Ask these four questions before writing anything:
      
      1. **What operation is dangerous?** (`eval()`, `system()`, raw SQL, an authorization check)
      2. **What data makes it dangerous?** (user-controlled input, a null, an attacker-chosen size)
      3. **What's missing?** (sanitization, validation, a bounds check, a null guard)
      4. **What context enables it?** (authentication state, an error path, a specific caller)
      
      Then formulate a statement:
      
      > "This vulnerability exists because [UNTRUSTED DATA] reaches [DANGEROUS OPERATION]
      > without [REQUIRED PROTECTION]."
      
      Examples:
      - "User input reaches `eval()` without sanitization"
      - "Attacker-controlled size reaches `malloc()` without an overflow check"
      - "Untrusted path reaches `open()` without canonicalization"
      
      For logic bugs that have no data flow, state the violated invariant instead: "this function
      must return False for unauthenticated callers, and it returns True when both IDs are null."
      
      That statement IS the search pattern. Everything below expands it.
      
      ## The Expansion Checklist
      
      A single root cause manifests in several ways. Enumerate all of them before searching.
      
      ### 1. Semantically related identifiers
      
      If the bug involves one name, every name that plays the same role is in scope:
      
      - `isAuthenticated`: also `isActive`, `isAdmin`, `isVerified`, `isLoggedIn`
      - `userId`: also `ownerId`, `creatorId`, `authorId`
      
      Ground these in the codebase. Grep for the names before claiming them: a list of plausible
      identifiers that don't exist wastes an entire search axis.
      
      ### 2. Other boolean-logic errors
      
      The same mistake in a different shape:
      
      - Inverted conditions (`if not x` where `if x` was meant)
      - Wrong default return (`return True` on the fall-through path)
      - Short-circuit evaluation errors (`or` where `and` was meant)
      
      ### 3. Data-type edge cases
      
      - Null/None/undefined comparisons, especially where *both* sides can be null
      - Empty string vs null
      - Zero vs null
      - Empty arrays and collections
      
      ### 4. Documentation/code mismatches
      
      A function whose behavior contradicts its own name or docstring. Search for functions named
      with `deny`, `restrict`, `block`, `forbid`, `check`, `validate` and confirm the return value
      means what the name says.
      
      ## What Makes a Good Axis
      
      Each axis is handed to a separate agent that knows nothing about the others. So an axis must be:
      
      - **Independently searchable** — it names concrete identifiers or code constructs to look for,
        not a theme like "authorization problems"
      - **Non-overlapping** — two axes that find the same code waste an agent
      - **Grounded** — its leads exist in this codebase
      
      ## Pitfalls at This Stage
      
      **Pattern too specific.** Using only the exact attribute from the original bug misses
      variants built on related constructs. Enumerate the whole family, not the one instance.
      
      **Single vulnerability class.** Focusing on one manifestation misses the others. A
      "returns allow when the condition is false" bug also hides as a null-equality bypass, a
      docs/code mismatch, and an inverted conditional. List every manifestation before searching.
      
      **Ungrounded axes.** Plausible-sounding identifiers that don't appear in the codebase
      produce an agent that searches hard and finds nothing.
      
    • searching.md 4.5 KB
      # Searching: The Abstraction Ladder
      
      Strategy for the sweep stage. You have a root cause and one axis to search. The job is to
      climb from a pattern that matches only the known bug to one that matches its variants,
      without climbing so far that the results become noise.
      
      ## Tool Selection
      
      | Scenario | Tool | Why |
      |----------|------|-----|
      | Quick surface search | ripgrep | Fast, zero setup |
      | Simple pattern matching | Semgrep | Easy syntax, no build needed |
      | Data flow tracking | Semgrep taint / CodeQL | Follows values across functions |
      | Cross-function analysis | CodeQL | Best interprocedural analysis |
      | Non-building code | Semgrep | Works on incomplete code |
      
      Use ripgrep for recon, Semgrep for iteration, CodeQL for precision. Tool loyalty is an
      anti-pattern: "I only use CodeQL" costs you the fast passes that tell you where to aim it.
      
      ## The Ladder
      
      ### Level 0: Exact match
      
      Match the literal vulnerable code.
      
      ```python
      # Original vulnerable code
      query = "SELECT * FROM users WHERE id=" + request.args.get('id')
      ```
      
      ```bash
      rg 'SELECT \* FROM users WHERE id=" \+ request\.args\.get'
      ```
      
      Matches 1, zero false positives. This is not a search, it is the calibration point that proves your understanding of the bug is correct.
      
      ### Level 1: Variable abstraction
      
      Replace variable names with metavariables.
      
      ```yaml
      pattern: $QUERY = "SELECT * FROM users WHERE id=" + $INPUT
      ```
      
      Matches 3-5, low FP. Finds copy-paste variants.
      
      ### Level 2: Structural abstraction
      
      Generalize the surrounding structure.
      
      ```yaml
      patterns:
        - pattern: $Q = "..." + $INPUT
        - pattern-inside: |
            def $FUNC(...):
              ...
              cursor.execute($Q)
      ```
      
      Matches 10-30, medium FP. Finds pattern variants.
      
      ### Level 3: Semantic abstraction
      
      Abstract to the security property itself.
      
      ```yaml
      mode: taint
      pattern-sources:
        - pattern: request.args.get(...)
        - pattern: request.form.get(...)
      pattern-sinks:
        - pattern: cursor.execute(...)
      ```
      
      Matches 50-100+, high FP. Comprehensive coverage, requires real triage.
      
      ### Choosing your level
      
      | Goal | Level |
      |------|-------|
      | Verify a specific fix | 0 |
      | Find copy-paste bugs | 1 |
      | Audit a component | 2 |
      | Full security assessment | 3 |
      
      ## One Change at a Time
      
      Never generalize multiple elements at once.
      
      ```
      BAD:  exact code -> fully abstract pattern
      GOOD: exact code -> abstract var1 -> abstract var2 -> abstract operation
      ```
      
      Each step: make ONE change, run it, read ALL new matches, decide whether the FP rate is
      still acceptable, then continue or revert. Jumping straight to Level 3 produces a pile of
      results with no way to tell which abstraction introduced the noise.
      
      ### Decision points
      
      **Abstract this variable name?** Yes if different names could carry the same bug. No if the
      name itself is the semantic constraint you are relying on.
      
      **Abstract this literal?** Yes if any value triggers the bug. No if only specific values are
      dangerous.
      
      **Use `...` wildcards?** Yes if argument position doesn't matter. No if only a specific
      position is a sink.
      
      **Add taint tracking?** Yes if you need to prove data actually flows from source to sink. No
      if the presence of the pattern is already sufficient evidence.
      
      ## Search Scope
      
      Run every search against the **entire codebase root**, not the directory the original bug
      lived in. A bug found in `api/handlers/` with a variant in `utils/auth.py` is the normal
      case, not the exotic one. Narrow scope is the single most common reason a hunt finds nothing.
      
      ## False Positive Management
      
      ### Acceptable rates by context
      
      | Context | Acceptable FP rate |
      |---------|-------------------|
      | Automated CI blocking | <5% |
      | Developer warning | <20% |
      | Security audit triage | <50% |
      | Research/exploration | <80% |
      
      For a variant hunt, stop generalizing when more than roughly half the matches are noise.
      That is the signal you climbed one level too far: revert and take a different abstraction
      rather than pushing further up the same one.
      
      ### Common FP sources and filters
      
      **Test code** — exclude test trees:
      ```bash
      rg "pattern" --glob '!**/test*' --glob '!**/*_test.*'
      ```
      
      **Already sanitized** — subtract the safe form:
      ```yaml
      pattern-not: dangerous_func(sanitize($X))
      ```
      
      **Literal values** — not attacker-controlled:
      ```yaml
      pattern-not: dangerous_func("...")
      ```
      
      **Dead code** — add reachability constraints:
      ```yaml
      pattern-not-inside: |
        if False:
          ...
      ```
      
      Analyze false positives as you go rather than deferring them. They tell you which
      abstraction was too aggressive, which is information you lose if you triage in a batch at the end.
      
    • triage.md 3.5 KB
      # Triage: Deciding Whether a Candidate Is Real
      
      Strategy for the verification stage. You have candidate locations that resemble a known
      bug. The job is to determine which ones actually carry it, and to say so with a severity
      attached.
      
      ## Argue Against the Candidate
      
      The snippet alone is never enough. Read the surrounding function, the callers, and the type
      of every value involved, then look specifically for the thing that makes it safe:
      
      - A guard earlier in the function or in a decorator/middleware
      - A sanitizer, validator, or parameterized API between source and sink
      - A type constraint that makes the dangerous value unreachable
      - A caller set that never supplies attacker-controlled input
      
      A candidate survives only if you looked for these and did not find them.
      
      Note what is *not* on that list: having no callers. Code that nothing reaches today is
      still unprotected code, and a variant hunt is exactly the search that finds it before a
      caller arrives. Report it at lower severity — see Exploitability below — rather than
      refuting it as dead.
      
      ## Exploitability
      
      For a surviving candidate, establish:
      
      - **Reachable** — is there a path from an external entry point to this code?
      - **Controllable** — can an attacker influence the value that makes it dangerous?
      - **Unprotected** — is the protection named in the root cause genuinely absent here?
      
      A candidate that is reachable and controllable but has a different protection in place is a
      false positive worth recording, not a finding. A candidate that is unreachable *today* but
      unprotected is a real finding at lower severity: say so explicitly, and say what would make
      it reachable.
      
      ## Edge Cases That Hide Real Bugs
      
      Test every candidate against these if applicable, because normal-path reasoning misses them.
      
      ### Null equality bypasses
      
      A common authorization bypass. If both sides of a comparison can be null at the same time,
      the comparison succeeds for the wrong reason:
      
      ```python
      # anonymous_user.id is None, guest_order.owner_id is None
      # None == None is True, so the check passes for a user who owns nothing
      if order.owner_id == current_user.id:
          return True
      ```
      
      Ask: what values can each side hold? Can both be null simultaneously? Who can cause that?
      
      ### Documentation/code mismatch
      
      The function does the opposite of what its name or docstring claims:
      
      ```python
      def check_restricted_permission(user, perm):
          """Returns True if access should be DENIED."""
          if user.has_perm(perm):
              return True   # BUG: returns "deny" for users who DO have permission
          return False
      ```
      
      Every caller of a function like this is a potential finding, even where the call site looks
      correct. Verify the return semantics against the name before trusting any use of it.
      
      ### Others worth testing
      
      - Unauthenticated and anonymous callers
      - Empty strings vs null, zero vs null
      - Empty arrays and collections
      - Boundary values at the limits of the type
      
      ## Severity and Confidence
      
      Attach a severity to **every** verdict, including informational ones. Do not suppress
      findings you judge minor: filtering happens downstream, where the full set is visible, and a
      finding you decline to mention is a finding nobody sees.
      
      Separate the two axes:
      
      - **Severity** — the impact if this is real
      - **Confidence** — how sure you are that it is real
      
      ## Recording False Positives
      
      When a candidate is ruled out, record *why* it was safe. Grouped by reason, these become the
      false-positive table in the report, and they are what lets the next stage refine the
      pattern instead of re-triaging the same matches.
      
  • resources
    • codeql
      • cpp.ql 3.4 KB · in bundle
      • go.ql 2 KB · in bundle
      • java.ql 2.3 KB · in bundle
      • javascript.ql 1.7 KB · in bundle
      • python.ql 2.4 KB · in bundle
    • semgrep
      • cpp.yaml 2.7 KB
        rules:
          - id: variant-taint-cpp
            message: "Potential variant: user input flows to dangerous sink"
            severity: ERROR
            languages: [c, cpp]
            mode: taint
        
            pattern-sources:
              # Command line
              - pattern: argv[$IDX]
              # Standard input
              - pattern: gets(...)
              - pattern: fgets($BUF, $SIZE, stdin)
              - pattern: scanf(...)
              - pattern: fscanf(...)
              - pattern: getenv(...)
              # Network
              - pattern: recv($SOCK, $BUF, ...)
              - pattern: recvfrom(...)
              - pattern: read($FD, $BUF, ...)
        
            pattern-sinks:
              # Command injection
              - pattern: system($SINK)
              - pattern: popen($SINK, ...)
              - pattern: execl($SINK, ...)
              - pattern: execlp($SINK, ...)
              - pattern: execv($SINK, ...)
              - pattern: execvp($SINK, ...)
              # Buffer overflow
              - pattern: strcpy($DST, $SINK)
              - pattern: strcat($DST, $SINK)
              - pattern: sprintf($DST, $FMT, ..., $SINK, ...)
              - pattern: gets($SINK)
              # Format string
              - pattern: printf($SINK)
              - pattern: fprintf($FILE, $SINK)
              - pattern: sprintf($BUF, $SINK)
              - pattern: syslog($PRI, $SINK)
              # Memory
              - pattern: malloc($SINK)
              - pattern: calloc($SINK, ...)
              - pattern: realloc($PTR, $SINK)
              - pattern: alloca($SINK)
              # File operations
              - pattern: fopen($SINK, ...)
              - pattern: open($SINK, ...)
        
            pattern-sanitizers:
              - pattern: strncpy($DST, $SRC, $N)
              - pattern: strncat($DST, $SRC, $N)
              - pattern: snprintf($BUF, $SIZE, ...)
              - pattern: strlcpy(...)
              - pattern: strlcat(...)
        
            paths:
              exclude:
                - "**/test/**"
                - "**/*_test.c"
                - "**/*_test.cpp"
        
          - id: unsafe-functions-cpp
            message: "Use of unsafe function - consider bounded alternative"
            severity: WARNING
            languages: [c, cpp]
            pattern-either:
              - pattern: gets(...)
              - pattern: strcpy(...)
              - pattern: strcat(...)
              - pattern: sprintf(...)
              - pattern: vsprintf(...)
        
          - id: format-string-cpp
            message: "Potential format string vulnerability"
            severity: ERROR
            languages: [c, cpp]
            patterns:
              - pattern-either:
                  - pattern: printf($VAR)
                  - pattern: fprintf($F, $VAR)
                  - pattern: sprintf($B, $VAR)
                  - pattern: snprintf($B, $S, $VAR)
              - pattern-not: printf("...")
              - pattern-not: fprintf($F, "...")
              - pattern-not: sprintf($B, "...")
              - pattern-not: snprintf($B, $S, "...")
        
          - id: integer-overflow-cpp
            message: "Potential integer overflow before memory allocation"
            severity: WARNING
            languages: [c, cpp]
            patterns:
              - pattern: |
                  $SIZE = $X * $Y;
                  ...
                  malloc($SIZE)
              - pattern: malloc($X * $Y)
              - pattern: calloc($X * $Y, ...)
        
      • go.yaml 1.8 KB
        rules:
          - id: variant-taint-go
            message: "Potential variant: user input flows to dangerous sink"
            severity: ERROR
            languages: [go]
            mode: taint
        
            pattern-sources:
              # net/http
              - pattern: $REQ.URL.Query().Get(...)
              - pattern: $REQ.FormValue(...)
              - pattern: $REQ.PostFormValue(...)
              - pattern: $REQ.Header.Get(...)
              # Gin
              - pattern: $CTX.Query(...)
              - pattern: $CTX.Param(...)
              - pattern: $CTX.PostForm(...)
              - pattern: $CTX.GetHeader(...)
              # Echo
              - pattern: $CTX.QueryParam(...)
              - pattern: $CTX.FormValue(...)
              # os.Args
              - pattern: os.Args[$IDX]
              - pattern: os.Getenv(...)
        
            pattern-sinks:
              # Command injection
              - pattern: exec.Command($SINK, ...)
              - pattern: exec.CommandContext($CTX, $SINK, ...)
              # SQL injection
              - pattern: $DB.Query($SINK, ...)
              - pattern: $DB.QueryRow($SINK, ...)
              - pattern: $DB.Exec($SINK, ...)
              # Path traversal
              - pattern: os.Open($SINK)
              - pattern: os.OpenFile($SINK, ...)
              - pattern: os.ReadFile($SINK)
              - pattern: ioutil.ReadFile($SINK)
              # Template injection
              - pattern: template.HTML($SINK)
        
            pattern-sanitizers:
              - pattern: strconv.Atoi($X)
              - pattern: strconv.ParseInt($X, ...)
              - pattern: filepath.Clean($X)
              - pattern: filepath.Base($X)
              - pattern: html.EscapeString($X)
        
            paths:
              exclude:
                - "**/*_test.go"
                - "**/test/**"
                - "**/vendor/**"
        
          - id: variant-pattern-go
            message: "Suspicious pattern matching known vulnerability"
            severity: WARNING
            languages: [go]
            patterns:
              - pattern-either:
                  - pattern: exec.Command(...)
                  - pattern: $DB.Query($Q, ...)
              - pattern-not: exec.Command("...")
        
      • java.yaml 2 KB
        rules:
          - id: variant-taint-java
            message: "Potential variant: user input flows to dangerous sink"
            severity: ERROR
            languages: [java]
            mode: taint
        
            pattern-sources:
              # Servlet
              - pattern: (HttpServletRequest $REQ).getParameter(...)
              - pattern: (HttpServletRequest $REQ).getHeader(...)
              - pattern: (HttpServletRequest $REQ).getCookies()
              - pattern: (HttpServletRequest $REQ).getQueryString()
              - pattern: (HttpServletRequest $REQ).getInputStream()
              # Spring
              - pattern: "@RequestParam $TYPE $VAR"
              - pattern: "@PathVariable $TYPE $VAR"
              - pattern: "@RequestBody $TYPE $VAR"
        
            pattern-sinks:
              # Command injection
              - pattern: Runtime.getRuntime().exec($SINK, ...)
              - pattern: new ProcessBuilder($SINK, ...)
              # SQL injection
              - pattern: (Statement $S).executeQuery($SINK)
              - pattern: (Statement $S).executeUpdate($SINK)
              - pattern: (Statement $S).execute($SINK)
              - pattern: (Connection $C).prepareStatement($SINK)
              # Path traversal
              - pattern: new File($SINK)
              - pattern: new FileInputStream($SINK)
              - pattern: new FileOutputStream($SINK)
              - pattern: Paths.get($SINK, ...)
              # XXE
              - pattern: (DocumentBuilder $DB).parse($SINK)
              # Deserialization
              - pattern: (ObjectInputStream $OIS).readObject()
        
            pattern-sanitizers:
              - pattern: Integer.parseInt($X)
              - pattern: Integer.valueOf($X)
              - pattern: StringEscapeUtils.escapeHtml4($X)
              - pattern: ESAPI.encoder().encodeForSQL(...)
        
            paths:
              exclude:
                - "**/test/**"
                - "**/*Test.java"
        
          - id: variant-pattern-java
            message: "Suspicious pattern matching known vulnerability"
            severity: WARNING
            languages: [java]
            patterns:
              - pattern-either:
                  - pattern: Runtime.getRuntime().exec(...)
                  - pattern: new ProcessBuilder(...)
              - pattern-inside: |
                  $RET $METHOD(..., HttpServletRequest $REQ, ...) {
                    ...
                  }
        
      • javascript.yaml 1.7 KB
        rules:
          - id: variant-taint-js
            message: "Potential variant: user input flows to dangerous sink"
            severity: ERROR
            languages: [javascript, typescript]
            mode: taint
        
            pattern-sources:
              # Express
              - pattern: req.query.$PARAM
              - pattern: req.body.$PARAM
              - pattern: req.params.$PARAM
              - pattern: req.cookies.$PARAM
              # URL/Location
              - pattern: window.location.$PROP
              - pattern: document.location.$PROP
              - pattern: location.search
              - pattern: location.hash
        
            pattern-sinks:
              # Command injection
              - pattern: child_process.exec($SINK, ...)
              - pattern: child_process.execSync($SINK, ...)
              - pattern: child_process.spawn($SINK, ...)
              # Code execution
              - pattern: eval($SINK)
              - pattern: Function($SINK)
              - pattern: setTimeout($SINK, ...)
              - pattern: setInterval($SINK, ...)
              # SQL
              - pattern: $DB.query($SINK, ...)
              - pattern: $DB.raw($SINK)
              # XSS
              - pattern: $EL.innerHTML = $SINK
              - pattern: document.write($SINK)
        
            pattern-sanitizers:
              - pattern: parseInt($X, ...)
              - pattern: encodeURIComponent($X)
              - pattern: escape($X)
              - pattern: $DB.escape($X)
        
            paths:
              exclude:
                - "**/*.test.js"
                - "**/*.spec.js"
                - "**/test/**"
                - "**/node_modules/**"
        
          - id: variant-pattern-js
            message: "Suspicious pattern matching known vulnerability"
            severity: WARNING
            languages: [javascript, typescript]
            patterns:
              - pattern-either:
                  - pattern: eval(...)
                  - pattern: Function(...)
                  - pattern: child_process.exec(...)
              - pattern-not: eval("...")
              - pattern-not: Function("...")
        
      • python.yaml 2 KB
        rules:
          - id: variant-taint-analysis
            message: >-
              Potential variant: user-controlled data flows to dangerous sink.
              Original bug: [DESCRIBE_ORIGINAL_BUG]
            severity: ERROR
            languages: [python]
            mode: taint
        
            pattern-sources:
              # Flask
              - pattern: request.args.get(...)
              - pattern: request.args[...]
              - pattern: request.form.get(...)
              - pattern: request.form[...]
              - pattern: request.json
              - pattern: request.data
              # Django (uncomment if needed)
              # - pattern: request.GET.get(...)
              # - pattern: request.POST.get(...)
              # General
              - pattern: os.environ.get(...)
              - pattern: input(...)
        
            pattern-sinks:
              # Command injection
              - pattern: os.system($SINK)
              - pattern: os.popen($SINK)
              - pattern: subprocess.call($SINK, ...)
              - pattern: subprocess.run($SINK, ...)
              - pattern: subprocess.Popen($SINK, ...)
              # Code execution
              - pattern: eval($SINK)
              - pattern: exec($SINK)
              # SQL (uncomment if needed)
              # - pattern: $CURSOR.execute($SINK)
              # Path traversal (uncomment if needed)
              # - pattern: open($SINK, ...)
        
            pattern-sanitizers:
              - pattern: shlex.quote(...)
              - pattern: os.path.basename(...)
              - pattern: int(...)
              - pattern: sanitize(...)
              - pattern: escape(...)
              - pattern: validate(...)
        
            paths:
              exclude:
                - "*_test.py"
                - "test_*.py"
                - "tests/"
                - "**/test/**"
        
            metadata:
              category: security
              confidence: HIGH
        
          # Simple pattern matching variant (non-taint)
          - id: variant-pattern-match
            message: "Suspicious pattern matching known vulnerability signature"
            severity: WARNING
            languages: [python]
            patterns:
              - pattern-either:
                  - pattern: dangerous_func($USER_DATA)
                  - pattern: risky_operation(..., $USER_DATA, ...)
              - pattern-not: dangerous_func("...")
            paths:
              exclude:
                - "tests/"
                - "*_test.py"
        
    • variant-report-template.md 1.3 KB
      # Variant Analysis Report
      
      ## Summary
      
      | Field | Value |
      |-------|-------|
      | **Original Bug** | [BUG_ID / CVE] |
      | **Analysis Date** | [DATE] |
      | **Codebase** | [REPO/PROJECT] |
      | **Variants Found** | [COUNT] |
      
      ## Original Vulnerability
      
      **Root Cause:** [e.g., "User input reaches SQL query without parameterization"]
      
      **Location:** `[path/to/file.py:LINE]` in `function_name()`
      
      ```python
      # Vulnerable code
      ```
      
      ## Search Methodology
      
      | Version | Pattern | Tool | Matches | TP | FP |
      |---------|---------|------|---------|----|----|
      | v1 | [exact] | ripgrep | 1 | 1 | 0 |
      | v2 | [abstract] | semgrep | N | N | N |
      
      **Final Pattern:**
      ```yaml
      # Pattern used
      ```
      
      ## Findings
      
      ### Variant #1: [BRIEF_TITLE]
      
      | Severity | Confidence | Status |
      |----------|------------|--------|
      | High | High | Confirmed |
      
      **Location:** `[path/to/file.py:LINE]`
      
      ```python
      # Vulnerable code
      ```
      
      **Analysis:** [Why this is a true/false positive]
      
      **Exploitability:**
      - [ ] Reachable from external input
      - [ ] User-controlled data
      - [ ] No sanitization
      
      ---
      
      <!-- Copy variant template above for additional findings -->
      
      ## False Positive Patterns
      
      | Pattern | Count | Reason |
      |---------|-------|--------|
      | [pattern] | N | [why safe] |
      
      ## Recommendations
      
      ### Immediate
      1. Fix variant in [location]
      
      ### Preventive
      1. Add Semgrep rule to CI
      
      ```yaml
      # CI-ready rule
      ```
      
  • SKILL.md 3.8 KB
    ---
    name: variant-analysis
    description: Hunts for the other instances of a bug already found — the variants of one root cause across a codebase. Use immediately after a vulnerability, logic bug, or bad pattern turns up in a specific file and the question becomes where else it occurs, including the bare conversational form ("are there others like this?", "is this the same bug?"). Also for generalizing one known instance into a CodeQL or Semgrep query for its whole pattern family, and for triaging a set of look-alike candidates against a known root cause. Not for initial discovery with no bug in hand.
    ---
    
    # Variant Analysis
    
    Find the other instances of a bug you have already found. One root cause usually has several
    manifestations, and they are rarely in the module where you found the first one.
    
    ## When to Use
    
    - A vulnerability has been found and you need to search for similar instances
    - Building or refining CodeQL/Semgrep queries for security patterns
    - Performing systematic code audits after an initial issue discovery
    - Analyzing how a single root cause manifests in different code paths
    
    ## When NOT to Use
    
    - Initial vulnerability discovery — use audit-context-building or a domain-specific audit
    - General code review with no known pattern to search for
    - Writing fix recommendations — use issue-writer
    - Understanding unfamiliar code — use audit-context-building first
    
    ## The Five Steps
    
    Read the reference for a step when you reach it.
    
    **1. Understand the original issue.** Extract the root cause — why the code is wrong, not
    what it does — and enumerate the directions a variant could hide in: related identifiers,
    other manifestations of the same mistake, data-type edge cases.
    → [references/root-cause.md](references/root-cause.md)
    
    **2. Create an exact match.** Write a pattern matching ONLY the known instance and confirm
    it hits. A pattern that matches nothing means you have misunderstood the bug, and every
    search built on it is calibrated against the wrong code.
    
    **3–4. Generalize one element at a time.** Climb from the exact match toward the pattern
    family, running and reading all matches after each single change. Stop when more than half
    the matches are noise.
    → [references/searching.md](references/searching.md) — abstraction ladder, tool selection,
    false-positive filters
    
    **5. Triage.** Decide which candidates are real, and say so with a severity attached.
    → [references/triage.md](references/triage.md)
    
    **Then write it up**, including the patterns that failed and a CI rule to prevent regression.
    → [references/reporting.md](references/reporting.md)
    
    ## Running it as a Workflow
    
    This plugin ships `/variant-analysis:variants`, which runs the five steps across parallel
    subagents — one per expansion axis, looping until the sweep stops finding anything new.
    Each stage reads the reference above that matches its job.
    
    Use the workflow when the codebase is large or the root cause has many manifestations. Work
    the steps directly when the search is narrow or you want a say in each generalization.
    
    ## What Makes Hunts Fail
    
    1. **Narrow scope** — searching only the module the original bug was in
    2. **Pattern too specific** — searching one attribute and missing the family around it
    3. **One vulnerability class** — chasing a single manifestation of the root cause
    4. **Happy-path testing** — never trying the null, empty, and boundary cases
    5. **Generalizing too fast** — abstracting several elements at once, so noise cannot be
       attributed to any one of them
    
    The first three are covered in root-cause.md and searching.md, the fourth in triage.md.
    
    ## Resources
    
    **CodeQL** (`resources/codeql/`): `python.ql`, `javascript.ql`, `java.ql`, `go.ql`, `cpp.ql`
    
    **Semgrep** (`resources/semgrep/`): `python.yaml`, `javascript.yaml`, `java.yaml`, `go.yaml`, `cpp.yaml`
    
    **Report**: `resources/variant-report-template.md`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related