<!-- Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com> -->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->

# Pentesting / Adversarial Suite

A separately-runnable harness that attacks the server the way a hostile client
would, and asserts the guarantees that keep a deterministic device safe. It is
**not** part of the per-commit unit-test run (`test/run_tests.sh`): it is heavier,
randomized, and meant to be run on demand or on a schedule.

> ## ⚖️ Authorized use only
>
> The live driver ([pentesting/detws_pentest.py](../pentesting/detws_pentest.py))
> and the on-device techniques below send **real attack traffic**. Use them **only**
> against a device you **own** or have **explicit, written permission** to test.
> Unauthorized access to or interference with computer systems is a crime in most
> jurisdictions (US CFAA, UK Computer Misuse Act, and equivalents). You, the
> operator, are solely responsible for staying within the law and the scope of your
> authorization. This tooling is provided **as is, with no warranty and no
> liability**. We build defensive tooling - use it for good. The live driver
> refuses to run until you pass `--authorized`.

There are three layers:

1. **Host fuzz** (`env:native_pentest`) - throws malformed, oversized, partial,
   binary, and deterministically-random input at the untrusted-input **parsers**
   and checks the safety invariants. Fast, reproducible, CI-friendly.
2. **Live adversarial driver** ([pentesting/detws_pentest.py](../pentesting/detws_pentest.py)) -
   a feature-aware Python tool that drives a flashed board over the network,
   auto-detects the build's features, runs the applicable attacks, and checks
   liveness + heap-drift after each one. See [pentesting/README.md](../pentesting/README.md).
3. **Manual on-device stress** - ad-hoc `curl`/`nc` techniques for one-off probing.

---

## The invariants under test

Every adversarial input must preserve all three. A violation is a finding.

| Invariant           | What it means                                                            | How it is checked                                                                                                                                          |
| :------------------ | :----------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Fixed footprint** | No fixed buffer index ever exceeds its compile-time bound (no overflow). | After every input: `path_idx <= MAX_PATH_LEN`, `header_count <= MAX_HEADERS`, `body_len <= BODY_BUF_SIZE`, response length `<= cap`, etc.                  |
| **Fail-closed**     | Bad input lands in a defined error state, never an undefined one.        | Parser state is always within the `ParseState` enum; bad requests reach `PARSE_ERROR` (400) / `PARSE_ENTITY_TOO_LARGE` (413) / `PARSE_URI_TOO_LONG` (414). |
| **Liveness**        | The parser always terminates - no hang, no over-read.                    | The harness feeds bounded streams and returns; a hang/crash/sanitizer trip fails the run.                                                                  |

These follow from the library's core promise: **no heap after `begin()`**, all
buffers statically sized, so the only way adversarial input can hurt the device is
an overflow or an undefined state - exactly what this suite hunts.

---

## 1. Host fuzz (`native_pentest`)

### Run it

```bash
pio test -e native_pentest
```

It is intentionally excluded from `test/run_tests.sh`, so a normal `pio test` /
CI test run does not pay its cost. Run it before a release, nightly, or after
touching any parser.

### What it covers

Targets (every codec that faces untrusted bytes from the wire or the user):

- **HTTP request parser** (`http_parser_feed`) - the primary attack surface.
- **multipart/form-data** (`multipart_parse`) - in-place body parsing.
- **CoAP datagram** (`coap_server_process`) - untrusted UDP, option parsing.
- **SNMP agent + ASN.1/BER** (`snmp_agent_process`, `ber_read_*`) - untrusted UDP.
- **Modbus** - slave ADU codec (`modbus_process_adu`) + master response parser
  (`modbus_parse_response`).
- **CBOR reader** (`cbor_read_*`) and **JSON reader** (`json_get_*`).
- **base64** (`base64_decode`) and **base32** (`detws_base32_decode`) decoders.

Attack classes (see `test/test_pentest/test_pentest.cpp`):

- **Oversized** - paths/queries/headers far past their bounds, giant
  `Content-Length` → must reach 414/413, never overflow.
- **Header flood** - hundreds of headers → `header_count` clamps to `MAX_HEADERS`.
- **Malformed** - empty input, missing method/version, bare LF, non-numeric
  `Content-Length`, high bytes in the path, the HTTP/2 `PRI * HTTP/2.0` preface.
- **Request smuggling** - duplicate `Content-Length`, CL.TE / TE.CL, obfuscated
  `Transfer-Encoding`, negative/hex/overflowing lengths, bad chunk sizes.
- **Header / target injection** - CR/LF/NUL in values, percent-encoded traversal
  and NUL, absolute-form and authority-form request targets.
- **Protocol confusion** - a TLS ClientHello fed to the cleartext parser.
- **Slowloris** - a never-terminated request fed one byte at a time.
- **Random fuzz** - tens of thousands of streams of random length and content,
  from a fixed PRNG seed (`0x1234567`), bounded-checked after each.
- **Structured fuzz** - a valid protocol skeleton (HTTP request, CoAP header,
  SNMP `SEQUENCE`) with random bytes spliced into the deeper fields, so the
  option/length/tag walkers are driven past the outer frame.

### Reproducing a finding

The fuzz PRNG is a deterministic `xorshift32` seeded in `setUp()`, so a failure is
fully reproducible: re-run `native_pentest` and it replays the identical streams.
To explore a different corpus, change the seed in `test_pentest.cpp`.

### Deeper: run under sanitizers

The host build can run under AddressSanitizer / UndefinedBehaviorSanitizer to turn
a silent over-read or UB into a hard failure. Add to the env's `build_flags` /
link flags:

```ini
build_flags = ... -fsanitize=address,undefined -fno-sanitize-recover=all
```

(Available on Linux/macOS clang or gcc; the same toolchain note as `native_tsan`
applies - not available on the MSVC-target Windows host, so run it in CI/WSL.)

### Adding a new target

The pattern is three lines per parser:

```cpp
// feed random/malformed bytes, then assert the safety invariant
size_t n = xs32() % sizeof(buf);
for (size_t i = 0; i < n; i++) buf[i] = (uint8_t)(xs32() & 0xFF);
int rc = my_parser(buf, n, out, sizeof(out));
TEST_ASSERT_TRUE(rc <= (int)sizeof(out));   // never writes past the buffer
```

The HTTP, multipart, CoAP, SNMP/BER, Modbus (slave + master), CBOR, JSON, base64,
and base32 codecs are covered. MessagePack is encode-only (no untrusted decoder),
so it is not a target. Good candidates still open: the WebSocket frame reassembler
(`ws_parse`) once a host harness for its rx ring exists, and the WebDAV / OPC UA
binary decoders.

---

## 2. Live adversarial driver (`pentesting/detws_pentest.py`)

The host fuzz proves the parsers are safe in isolation; the live driver proves the
**running device** is - its connection pool caps, its throttles and lockout engage,
it fails closed on the wire, and its **free heap does not drift** (the
"no heap after `begin()`" promise). It is feature-aware: point it at a device and
it reads `/diag` to learn which features are built in, then runs only the
applicable attacks.

```bash
# auto-detect features from /diag and run the applicable suite
python3 pentesting/detws_pentest.py --host 192.168.1.85 --diag --authorized

# or declare your flags (for features /diag does not enumerate), and go hard
python3 pentesting/detws_pentest.py --host 192.168.1.85 \
  --flags DETWS_ENABLE_AUTH,DETWS_ENABLE_ACCEPT_THROTTLE,DETWS_ENABLE_COAP \
  --intensity high --json report.json --authorized

# list every attack and its required feature, no target needed
python3 pentesting/detws_pentest.py --list
```

It checks two oracles around **every** attack - liveness (a legitimate `GET /`
still succeeds) and heap-drift (sampled from `/health`, `/stats`, `/metrics`, or
`/diag`) - plus per-attack fail-closed assertions (414/413/401/429/403/416/...).
The exit code is non-zero when any finding exists, so it drops into a CI gate. It
is Python-3-stdlib-only (no `pip` installs). Full documentation, the attack
catalog, and how to extend it: [pentesting/README.md](../pentesting/README.md).

---

## 3. Manual on-device stress (live server)

Flash a build with the listeners and defenses you want to exercise, then drive it
from a host on the same network. The goal: confirm the device **stays up and
fails closed** under abuse, within its fixed footprint.

Enable the relevant defenses in `ServerConfig.h` / build flags:

- `DETWS_ENABLE_ACCEPT_THROTTLE` (+ `DETWS_ACCEPT_THROTTLE_MAX/_WINDOW_MS`)
- `DETWS_ENABLE_PER_IP_THROTTLE`
- `DETWS_ENABLE_AUTH_LOCKOUT`
- `DETWS_ENABLE_IP_ALLOWLIST`
- `DETWS_ENABLE_CSRF`

### Example attacks

```bash
IP=192.168.1.85

# Slowloris: many half-open connections that dribble headers forever.
for i in $(seq 1 50); do
  (printf 'GET / HTTP/1.1\r\nHost: x\r\n'; sleep 60) | nc $IP 80 &
done
# Expect: connections capped at MAX_CONNS; the idle-timeout sweep reaps stalled
# ones; the device keeps serving fresh requests; heap never moves.

# Connection flood: rapid-fire accepts to trip the accept throttle.
for i in $(seq 1 500); do curl -s -o /dev/null --max-time 2 http://$IP/ & done; wait
# Expect: excess accepts dropped (RST) by the throttle; no crash.

# Oversized request line.
python3 -c "print('GET /' + 'a'*100000 + ' HTTP/1.1')" | nc $IP 80
# Expect: 414 URI Too Long, slot freed.

# Auth brute force (with DETWS_ENABLE_AUTH_LOCKOUT).
for i in $(seq 1 50); do curl -s -o /dev/null -u admin:wrong$i http://$IP/secure; done
# Expect: 429 + Retry-After after the threshold; later attempts locked out.
```

### What to watch

- **Heap is flat.** Poll a health endpoint (`DETWS_ENABLE_GUARDRAILS` → `/health`,
  or `DETWS_ENABLE_METRICS`) before/during/after - `free_heap` must not drift.
- **The device keeps serving.** A legitimate request still gets a normal response
  throughout the attack.
- **Defenses engage.** 429/RST/414/413 where expected; the allowlist rejects
  off-list sources at accept time.

> Modbus, SNMP, and CoAP have no authentication - only expose them on a trusted
> network, and pentest accordingly (malformed PDUs, oversized datagrams).

---

## Interpreting results

- **Host fuzz green** = the parsers held every invariant across the corpus.
- **A crash / hang / sanitizer trip** = a finding; capture the seed and the failing
  vector, fix the parser to fail closed, add the vector as a regression case.
- **On-device:** any heap drift, a wedged server, or a defense that did not engage
  is a finding.

The suite is a floor, not a ceiling - widen the corpus, add targets, and run it
under sanitizers in CI for the strongest assurance.
