# 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 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.

- **Kind:** skill
- **Source:** https://github.com/trailofbits/skills
- **Page:** https://forefy.com/skills/d06e61e3-51e9-49e3-a12b-88c206d2e976
- **API (JSON + files):** https://forefy.com/api/asr/d06e61e3-51e9-49e3-a12b-88c206d2e976

---

## SKILL.md

---
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`

## agents

```

```

## agents/openai.yaml

```yaml
interface:
  icon_small: "assets/trail-of-bits-mark.svg"
  icon_large: "assets/trail-of-bits-mark.svg"
  brand_color: "#D83A34"
```

## assets

```

```

## assets/trail-of-bits-mark.svg

```

```

## references

```

```

## references/reporting.md

# 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.

## references/root-cause.md

# 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.

## references/searching.md

# 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.

## references/triage.md

# 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

```

```

## resources/codeql

```

```

## resources/codeql/cpp.ql

```

```

## resources/codeql/go.ql

```

```

## resources/codeql/java.ql

```

```

## resources/codeql/javascript.ql

```

```

## resources/codeql/python.ql

```

```

## resources/semgrep

```

```

## resources/semgrep/cpp.yaml

```yaml
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, ...)
```

## resources/semgrep/go.yaml

```yaml
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("...")
```

## resources/semgrep/java.yaml

```yaml
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, ...) {
            ...
          }
```

## resources/semgrep/javascript.yaml

```yaml
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("...")
```

## resources/semgrep/python.yaml

```yaml
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"
```

## resources/variant-report-template.md

# 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
```

