Skip to content

Research and Probing Guide

Mikhail Deynekin edited this page Oct 14, 2026 · 1 revision

Research and Probing Guide

Phase 1 in full detail: how to inventory a subject, stage and run it, fire the probe batteries, write custom suites, handle stateful surfaces, and distil everything into the five research files that make the rest of the run — and any recovery from interruption — cheap. Every technique on this page was executed during this wiki run against the kit itself and, as a live battery demo, against MD.Viewer 2.18.0 (evidence: research/evidence/probe-mdviewer.json, test T14).

Contents


Step 1 — inventory before reading

scan/repo_scan.py --brief replaces an hour of manual grepping with two minutes of reading. What the brief surfaces, and why each item changes what you write later:

Brief section Why it matters
git metadata including tag count Zero tags means versioning must be documented from changelog/commit markers, not releases — the subject of this wiki has exactly that shape (8 commits, no tags)
license type and extra attribution clauses extra clauses change the redistribution advice you can give
declared dependencies or their absence "NO MANIFEST — zero declared dependencies" is itself a headline fact (supply-chain posture)
largest files where the logic lives; your reading order
entry points what to probe
version markers per-file versions (e.g. a viewer at 2.18 and an updater at 3.14 in one repo)
every external host your supply-chain and privacy facts, with the file each URL appears in
security-signal counts 11 regex classes; a spike in raw-html-passthrough points straight at the main security finding to verify
configuration-key surface the skeleton of your Configuration Reference page

Read the source in ranges — sed -n '661,760p' file — never cat a 200 KB file (PITFALLS B7). Start with header docblocks: in well-maintained codebases they hold a prose changelog with the reasoning behind decisions. This kit's own docblocks are extreme examples — lib/serve.py opens with the incident that made it necessary.

Step 2 — stage the subject, do not trust a running one

import sys; sys.path.insert(0, 'toolkit/lib')
from serve import ServedApp

with ServedApp('repo/SUBJECT', copy=['md.php', 'css', 'js']) as app:
    app.write('md.md', '# Title\n\nbody\n')      # fixture document
    r = app.get('/md.php')
    print(r.status, r.header('Content-Type'))
    app.set_ini({'DISABLE_UPLOAD': 'false'})      # flip a config flag
    print(app.ls())                               # what did the app write to disk?

ServedApp guarantees the four properties that hand-rolled serving destroys: a clean docroot (staged copy in a temp dir — no state contamination), a verified-free port (a busy preferred port raises; it never silently drifts to another server), readiness (waits for the socket, then fires one HTTP request to prove the interpreter is not dying on every request), and teardown (kills its own process group, deletes the staging dir unless keep=True).

Non-PHP subjects only need a command template:

python3 lib/serve.py repo/SUBJECT --serve-cmd 'node {root}/server.js' --port 0
python3 lib/serve.py repo/SUBJECT --serve-cmd 'python3 -m http.server {port} -d {root}' --check /index.html

Verified (T10): the http.server template stages, serves and tears down cleanly — the kit is not PHP-bound, PHP is only the default.

Caution

Before probing, check the runtime floor: python3 setup/check_runtime.py repo/SUBJECT exits 2 when the installed interpreter is below the subject's declared minimum. Probing a subject that cannot boot measures a corpse — every request returns a 500 and an unwary agent documents the error page as behaviour (PITFALLS B10).

Step 3 — fire the batteries

python3 probe/webapp_probe.py --repo repo/SUBJECT \
    --copy 'md.php,updater.php,css,js' --entry /md.php \
    --suite php-mdviewer \
    -o research/probe.json --md research/probe.md

What one run records per request: group, name, method, path, status, milliseconds, byte count, all response headers, a 180-character body excerpt, and — when you pass expect= — whether the status matched. The five batteries:

Battery Question it answers Classification rule
battery_headers Which of 10 security headers are present/MISSING; which of 4 leak headers disclose stack info; cookies presence check, case-insensitive
battery_traversal Does the path validator hold? (20 payloads through {PAYLOAD} templates) body markers, not status — a refusal may be HTTP 200 with a denial banner (deliberate non-disclosure); leak markers checked separately
battery_injection Does output escaping hold? (26 payloads through post/get/raw sinks) verdicts: REFLECTED-VERBATIM / escaped / reflected-case-changed / TAG-SURVIVED / SSTI-49 / empty-body / not-reflected
battery_flags Which config flags gate which endpoints? flip INI keys via the staged app, re-request, compare to expect
battery_disk What did the app write to disk as a side effect? full staged-tree listing

Measured example (T14, MD.Viewer 2.18.0 under PHP 8.2.34): the whole php-mdviewer suite — modes, traversal, injection, flags, API-key shape (mdv_ + 48 hex), a 16-filename upload matrix, disk side-effects — completed in 0.46 seconds, with 19 of 26 injection payloads reflected verbatim and 17 of 20 traversal payloads denied (the remaining three: two harmless 301 normalisations and one NUL-byte transport error). The suite also flagged two expectation mismatches against figures recorded from an older subject build — the expected-vs-actual column is how drift becomes visible instead of silent.

Step 4 — write a custom suite

A suite is a function, not a framework:

def suite_my_subject(probe, entry):
    out = {}
    probe.run("landing page", entry, expect=200, group="modes")
    probe.run("health endpoint", "/healthz", expect=200, group="modes")
    out["traversal"] = probe.battery_traversal(
        ["/files/{PAYLOAD}"],
        deny_markers=["not found", "denied"],       # BODY markers
    )
    out["injection"] = probe.battery_injection(
        [{"name": "search", "path": "/search", "field": "q", "via": "post"}],
    )
    out["headers"] = probe.battery_headers([entry, "/healthz"])
    out["files_written"] = probe.battery_disk()
    return out

SUITES["my-subject"] = suite_my_subject   # register next to generic/php-mdviewer

Rules learned in the field:

  1. Never trust a 200 without a body marker. A front controller answers 200 for almost any path because the router falls through to the home page; the first "successful" probe of the MD.Scribe run was exactly that — a correct status code and a meaningless result. Assert on a substring of the body.
  2. Two deployment layouts = two staged instances in one run. When a subject ships a public/ docroot and root-level compatibility shims, some endpoints are physically unreachable in one layout. ServedApp instances compose:
with ServedApp(repo, copy=['public', 'src'], serve_cmd='php -S {host}:{port} -t {root}/public router.php') as A, \
     ServedApp(repo, copy=['md.php', 'src'], serve_cmd='php -S {host}:{port} -t {root}') as B:
    pass  # probe A's contract and B's contract separately
  1. Ordered, session-bearing flows use lib/session.py, not batteries (batteries are order-independent): setup → login (CSRF via csrf_from_html, six printed shapes) → authenticated actions → logout, with the 302 observable (no auto-redirect), the cookie jar handling multiple Set-Cookie headers, and the token placed in the body field and a header — never the URL. One missed token shape cascades into dozens of 403s (the run-2 lesson).
  2. Write evidence through lib/evidence.write_evidence() so a filesystem hiccup cannot lose a completed run — it retries into <kit>/workspace-fallback/ instead of raising.

The full worked example of all four rules is examples/run2-mdscribe/probe_mdscribe.py (1,571 lines, 249 checks, two layouts, a stateful admin flow).

Step 5 — CLI-shaped and library-shaped subjects

For a CLI, skip serve.py and keep the shape that matters — declarative cases → evidence JSON → Markdown tables:

import json, subprocess
cases = [
    (["tool", "--help"], 0, "usage:"),
    (["tool", "--version"], 0, r"\d+\.\d+"),
    (["tool", "bad-flag"], 2, "unknown option"),
]
results = []
for argv, want_rc, want_out in cases:
    p = subprocess.run(argv, capture_output=True, text=True)
    results.append({"argv": argv, "rc": p.returncode, "rc_ok": p.returncode == want_rc,
                    "out_ok": want_out in p.stdout + p.stderr})
json.dump(results, open("research/cli-probe.json", "w"), indent=1)

For a library, the documented recipes are the probe surface: execute every snippet from the README in a clean venv and record pass/fail — that is how "the docs say X" becomes "X was reproduced".

Step 6 — the five research files

Write them before moving on; they are the recovery point if context is compacted, and Phase 3 works from them, not from your memory.

File Contents Failure mode it prevents
summary.md what it is, the problem, architecture in one paragraph, security posture, measured feature support, competitive position re-deriving the framing mid-write
architecture.md component map, request lifecycle, module breakdown, config resolution, function index with line numbers citing file:line from memory (they drift)
domain-notes.md standards context, security domain, deployment context, canonical glossary five terms for one concept across 20 pages
facts.json every value with a source: code:file:line | readme | git | test:name | web:url "was this verified or assumed?" becoming unanswerable
sources.md §A citations · §B named empirical tests · §C external sources · §D claims deliberately NOT made a future pass re-adding a plausible falsehood

Provenance discipline in facts.json (shape used by the reference run and this one):

{
  "injection_verbatim": {
    "value": "19 of 26 payloads reflected verbatim by the clipboard-preview sink",
    "source": "test:t14-probe-mdviewer.json"
  },
  "no_tags": { "value": true, "source": "git" },
  "runtime_floor": { "value": "Python 3.8+ (claimed)", "source": "readme" }
}

Mark every behavioural claim in summary.md [tested] or [read] — the distinction is the one rule made visible.

External research — the narrow gate

Web research is allowed only for context the repository cannot give you: specifications (GFM, CommonMark), platform behaviour (GitHub wiki rendering rules), standards (Agent Skills frontmatter), security references (OWASP), and competitor documentation. Every statement about the subject itself comes from the subject — a competitor comparison cites the competitors' docs, never their marketing claims about the subject. Record each external source in sources.md §C with its URL and what it was used for.


See also

Clone this wiki locally