# sec-semgrep — three Semgrep rules that tell a real bug from a look-alike

Three JDBC/Java sinks, three deliberate false-positive twins. The brief is the same text as
the page (`illustrated/10-security/exercise-semgrep-rules.html`).

This one runs on your Mac, not in the browser: Pyodide has no `semgrep` binary.

## What to implement

Three Semgrep rule files under `src/sec_semgrep/rules/`, each `mode: taint` (a bare
`pattern:` cannot tell a request parameter from a string literal — that's the whole point of
the false-positive fixture next to every true-positive one):

- **`deserialization.yaml`** (`fixtures/tp/DeserializeUntrusted.java` vs.
  `fixtures/fp/DeserializeLocalFile.java`) — flag `new ObjectInputStream(...)` when the stream
  comes from a socket or a servlet request; ignore one built from a local file.
- **`sqli.yaml`** (`fixtures/tp/SqlInjectionConcat.java` vs.
  `fixtures/fp/SqlParameterized.java`) — flag a request parameter that reaches a SQL string via
  concatenation. That string can be handed to JDBC two ways: straight to
  `Statement.executeQuery(sql)`, or to `Connection.prepareStatement(sql)` *before* any `?`
  placeholder binding happens — the second one still looks like "the safe API" and is not.
  Ignore a query built from a literal `"... WHERE x = ?"` with `setString`.
- **`exec_taint.yaml`** (`fixtures/tp/ExecTainted.java` vs. `fixtures/fp/ExecSafe.java`) — flag
  `Runtime.getRuntime().exec(...)` when the command comes from a request parameter; ignore a
  hardcoded command string.

The starter's three rule files load as `mode: taint` with a source but no `pattern-sinks` —
Semgrep refuses to run a taint rule with no sink, so every test fails until you add one.

## Run it

```
cd exercises/sec-semgrep && uv sync && uv run pytest -q
```

`uv sync` installs `pytest` and `semgrep` into `.venv/`. Each test shells out to the real
`semgrep` binary with exactly one rule file and one fixture file, so a mistake in one rule
cannot make an unrelated test fail.

## The checks (`tests/test_rules.py`)

- `test_deserialization_flags_untrusted_stream` / `test_deserialization_ignores_local_file`
- `test_sqli_flags_concatenated_prepared_statement` / `test_sqli_ignores_parameterized_query`
- `test_exec_flags_tainted_command` / `test_exec_ignores_hardcoded_command`

Each pair is the same rule against its true-positive and false-positive fixture: the first
assert is "exactly 1 finding", the second is "exactly 0". Read the fixture `.java` files
before writing a rule — the class and method names describe the vulnerability class, not the
line that matters.

Done when `uv run pytest -q` prints **6 passed**, then press "mark done" on the page.

## If you get stuck

- **`deserialization.yaml`** — `pattern-sources: [{pattern: $SOCK.getInputStream()}, {pattern:
  $REQ.getInputStream()}]`, `pattern-sinks: [{pattern: "new ObjectInputStream($X)"}]`.
- **`sqli.yaml`** — the trap most people fall into is a single sink on `executeQuery($SQL)`.
  `PreparedStatement.executeQuery()` takes **no argument** — the tainted string was already
  consumed by `prepareStatement($SQL)` a line earlier, so that call is its own sink.
- **`exec_taint.yaml`** — `pattern-sinks: [{pattern: $RT.exec($CMD)}]`; source is
  `$REQ.getParameter(...)`. Reaching for a bare `pattern:` here is what makes it fire on the
  hardcoded `ls` command too — that trap is why the false-positive fixture exists.
- **Reading a red row** — `uv run semgrep --config src/sec_semgrep/rules/<name>.yaml
  fixtures/<tp|fp>/<File>.java --json` shows exactly what a rule matched, without pytest's
  summary in the way.
