No description
  • Python 90%
  • Java 10%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Gabriel GRONDIN d094b15fb4
Some checks failed
CI / gate (push) Failing after 16s
feat(sdk): surface a trap-target-missing advisory on snmp writes
- Name the trap fields a trap-enabling write would leave empty.
- Decide it from the pre-write read, so it costs no extra request.
- Report it as data rather than raising on a recoverable condition.
- Keep it out of is_faithful, which judges device fidelity alone.
- Stay silent on a write that never touches the trap block.
- Soften the wire description that over-claimed the device check.
2026-07-28 23:56:04 +02:00
.github/workflows fix(ci): run the test suite from the python packages path 2026-07-22 14:38:44 +02:00
docs docs(rotation): add versioned target spec for password rotation 2026-07-27 16:24:54 +02:00
gradle/wrapper build(gradle): add a Gradle wrapper for repo-root builds 2026-07-23 10:52:04 +02:00
java/peplink-fiwm-loader docs(loader): retarget the build docs to Ghidra 12.1.2 2026-07-23 10:52:32 +02:00
python feat(sdk): surface a trap-target-missing advisory on snmp writes 2026-07-28 23:56:04 +02:00
.env.example chore: add the cli env-var example 2026-07-21 12:51:58 +02:00
.gitignore feat(cli): add the shared data-directory layout 2026-07-24 11:06:16 +02:00
.mcp.json chore: add the ghidra-mcp server config 2026-07-21 16:30:56 +02:00
.mmcp.toml chore: bind repository to the mmcp project group 2026-07-17 15:14:15 +02:00
build.gradle build(gradle): add a Gradle wrapper for repo-root builds 2026-07-23 10:52:04 +02:00
CLAUDE.md chore: bind repository to the mmcp project group 2026-07-17 15:14:15 +02:00
gen-config.yaml build: add openapi client generation config 2026-07-17 15:14:28 +02:00
gradle.properties build(gradle): default the Ghidra target to 12.1.2 2026-07-23 10:51:48 +02:00
gradlew build(gradle): add a Gradle wrapper for repo-root builds 2026-07-23 10:52:04 +02:00
gradlew.bat build(gradle): add a Gradle wrapper for repo-root builds 2026-07-23 10:52:04 +02:00
LICENSE docs: add project license and readme 2026-07-17 17:00:01 +02:00
openapi.yaml feat(sdk): surface a trap-target-missing advisory on snmp writes 2026-07-28 23:56:04 +02:00
pyproject.toml feat(cgibox): add the dispatch-table API-surface enumerator 2026-07-22 07:28:37 +02:00
README.md docs(readme): describe pepapy at its target 2026-07-22 14:38:44 +02:00
settings.gradle build(gradle): root workspace including the Ghidra loader subproject 2026-07-23 10:01:09 +02:00
uv.lock build: sync uv.lock 2026-07-28 23:37:27 +02:00

pepapy

pepapy is a version-aware, feature-gated Python SDK for the on-device Peplink Router API. Its entire surface is derived offline, by reverse-engineering firmware images for our own devices, never by probing a live router. A per-firmware OpenAPI spec is the single source of truth, and every other artifact is regenerated from it.

This targets the Router API served by the device itself, not the InControl 2 cloud API.

Exhaustive, version-aware coverage

pepapy models every command a firmware really exposes, not only the documented ones. Coverage spans the six Router API namespaces: auth, cmd, config, status, info, and system. Each operation records its path, method, parameters, response schema, and the firmware version window in which it exists.

Per-firmware OpenAPI specs are the source of truth

Each firmware version has its own OpenAPI 3.1 spec, and that spec is the only hand-edited artifact. Every operation carries x-peplink-available-since, and x-peplink-removed-in when a command is withdrawn. The union of the per-version specs reconstructs the full availability matrix and drives the version gating. Every derived file is regenerated from the specs and never hand-edited: the transport client, the capability gating table, and the operation-path map.

Recovered offline from firmware

The API surface and its schemas come from firmware extraction and handler decompilation, never from a device. A packaged toolchain covers the whole chain:

  • The FIWM firmware container and its embedded ext2 image.
  • LUKS-serpent decryption of the encrypted rootfs on firmware 8.1 and later.
  • A reader for the vendor's big-endian SquashFS 3.0 raw-LZMA1 variant.
  • Enumeration of the cgibox dispatch table that routes every command.
  • A Ghidra loader for the FIWM format, so the firmware opens directly for analysis.

Exact client, gated by version

pepapy-client is regenerated one-to-one from the specs and is never hand-edited. The same pass emits the capability gating table and the operation-path map, so the client, its gates, and its tests never drift from the spec.

Least-blocking, idempotent SDK

The SDK is the single home for every device interaction and every domain decision. One operation yields exactly one outcome: a success payload, a validation result, or a typed error. A batch never aborts on one item; it returns one result per item, recording which succeeded and which failed and why. Operations are idempotent wherever the domain allows, and version-aware about which commands exist on the target firmware.

Fleet credential rotation

pepapy rotates the administrative credential across the whole fleet, firmware 7.1.x through 8.5.x, dispatched by version. Below firmware 8.0.1 it uses the staged, full-replace config.admin path. At and after firmware 8.0.1 it uses the immediate, self-service cmd.password path. Rotation runs in dry-run, stage, apply, and discard-pending modes, and can export the result to a password-manager CSV.

Mutation and secret safety

config.* writes are full-replace, so an omitted field is wiped. Every such write is staged, the staged config is diffed against the running config, applied only on a clean diff, and discarded on every other exit. No live secret is ever read, logged, or returned, and sessions and cookies stay local.

Separate packages, one source of truth

The repository is a uv workspace of five independently publishable Python packages plus a Ghidra loader. The generated client, the SDK, and the testing library stay separate, and every command is testable against mocks. Every caller, the CLI and the research scripts, routes through the SDK rather than duplicating its logic.

Repository layout

graph TD
    root["pepapy/ (uv workspace root)"]
    root --> spec["openapi.yaml: OpenAPI 3.1 source of truth"]
    root --> gencfg["gen-config.yaml: client generator config"]
    root --> py["python/"]
    root --> java["java/peplink-fiwm-loader: Ghidra loader for the FIWM format"]

    py --> pkgs["packages/"]
    py --> scripts["scripts/"]

    pkgs --> client["pepapy-client: generated transport client (never hand-edited)"]
    pkgs --> sdk["pepapy: the SDK"]
    pkgs --> testing["pepapy-testing: mocking and unit-testing library"]
    pkgs --> cli["pepapy-cli: thin CLI over the SDK"]
    pkgs --> conf["pepapy-config: the .conf configuration library"]

    scripts --> fw["peplink_fw: FIWM, ext2, and SquashFS extraction"]
    scripts --> luks["luks_serpent: LUKS-serpent rootfs decryption"]
    scripts --> cgibox["peplink_cgibox: cgibox dispatch enumeration"]
    scripts --> adminpw["admin_password: admin credential research"]
    scripts --> cmdpw["cmd_password_probe: cmd.password probe"]
    scripts --> pepacli["pepacli.py: dev CLI (config, codegen)"]
    scripts --> devtools["devtools/codegen.py: regeneration from the specs"]

Source of truth and regeneration

graph TD
    spec["openapi.yaml per-firmware specs (source of truth)"]
    spec -->|codegen --client| client["pepapy-client (generated)"]
    spec -->|codegen| caps["pepapy capability gating table"]
    spec -->|codegen| ops["pepapy-testing operation-path map"]
    sdk["pepapy (SDK)"] -->|depends on| client
    testing["pepapy-testing"] -->|depends on| sdk

Regenerate every derived artifact from the specs with the dev CLI:

uv run python python/scripts/pepacli.py codegen --client

Omit --client to refresh only the capability gating table and the operation-path map.

The SDK surface, authentication modes, and version gating live in the SDK package README. See python/packages/pepapy/README.md.

Standards

pepapy holds to full mmcp-rule compliance, SSOT, and DRY. Names state exactly what the code does, one type lives per file, and comments are concise. Prose is one sentence per line, and no warning is tolerated.