Repository navigation
Releases
v0.1.9
A single fix to the Java attack-surface lens.
Fixed
- Sinks called on a
try-with-resources variable are no longer missed.modscan-audit --language javaresolves a call's receiver type from the file's declarations, and a variable declared in atry (ObjectInputStream in = …)header — a distinct AST node — was not read, soin.readObject()went unreported. Found by a new Semgrep cross-check against commons-lang3.
Full notes: CHANGELOG
v0.1.8
Adds the Java security lens and makes the ranking honest about ties.
Added
--versionon bothmodscanandmodscan-audit.- Tie disclosure.
detectand the benchmark now state when a score sits in a tie the alphabetical tiebreak decides, rather than presenting it as a ranking.--jsoncarriestied_withper point. modscan-audit --language java— a Java risk-sink catalog modelled on find-sec-bugs: ScriptEngine/Groovy/SpEL, ObjectInputStream/XMLDecoder/XStream/SnakeYAML, Runtime.exec/ProcessBuilder, Class.forName. Not yet cross-checked against an external authority (find-sec-bugs reads bytecode, MODScan reads source) — treat it as newer than the Python and TypeScript catalogs. Needspip install modscan[java].
Fixed
@overloadstubs no longer report a symbol several times. SQLAlchemy dropped 91 phantom candidates; medians improved, recall unchanged.
Full notes: CHANGELOG
v0.1.7
A third language, a second lens over MCP, and two TypeScript front-end defects found by pointing the benchmark at real packages.
Added
- Java front-end —
modscan detect --language java. Java maps onto the model more directly than any front-end so far:class/interface/abstract classare already its vocabulary,extends/implementsare bases, annotations play the decorator role, and visibility comes from the explicitpublicmodifier. Generic bases are reduced to their bare name, annotation arguments dropped, and nested classes deliberately not emitted as top-level seams. Optional:pip install modscan[java]. - The security lens over MCP —
modscan-mcpgainsaudit_attack_surfacebesidedetect_extension_points, returning exactly the payloadmodscan-audit --jsonemits so the non-coverage disclaimer travels with the data. - The security lens covers TypeScript/JavaScript —
modscan-audit --language typescriptmapseval,Function, thevmmodule,child_process, dynamicrequire/importand string-bodied timers. Modelled oneslint-plugin-security: a computedrequire(name)is a sink, a literalrequire('fs')is not. - More process sinks, and
shell=Trueraises severity —os.popen,os.startfile, theos.exec*/os.spawn*family andcommands.getoutput, found by cross-checking the lens against Bandit. Asubprocess.*call passing a literalshell=Truenow rates high.
Fixed
- CommonJS exports are recognised. The TS/JS front-end marked symbols public only from the ESM
exportkeyword, so every CommonJS file — most of npm — reported zero public symbols and contributed no extension points at all. .d.tsdeclaration files are no longer scanned. A declaration file declares types for code defined elsewhere, so scanning it produced a phantom copy of every symbol and let declaration-only option bags outrank the classes a project's documentation tells you to subclass.
Note
A modscan upgrade that adds catalogue entries can make the attack-surface gate report sinks that were always in the code — new to the detector, not to the codebase. Pin the Action's modscan-version for a baseline that only moves when you choose.
Full diff: v0.1.6...v0.1.7
v0.1.6
The security lens grows a memory: it can now compare two snapshots and gate a pull request on the execution sinks it introduces.
Added
- Attack-surface gate GitHub Action (
Rinkia/modscan/attack-surface) — on a pull request it audits the PR and the base branch, diffs them, sticky-comments the result, and fails the check when the PR introduces new execution sinks. It gates the delta, never the standing surface: sinks already on base never fail it.fail-ondefaults to high (eval/exec/pickle.loads/yaml.load/os.system) — measured across real packages, the medium tier is dominated by routine__reduce__, dynamic-import and subprocess code, so gating on it is noisy for exactly the plugin hosts MODScan serves. No LLM, no API key, no committed snapshot. MODScan runs it on itself. modscan-audit --diff BASE PR— compare two--jsonattack-surface snapshots and report the sinks the second introduces. Identity is(id, module, call)compared as a counted multiset, so moved code never registers as a change while a thirdevaladded to a module that already had two still shows up.modscan-audit --fail-on {high,medium,low,none}— defaults tonone: the CLI stays a report, and the policy lives in the gate.
Still enumeration, not taint: an introduced sink is new attack surface to review, and a passing check is not a clean bill of health.
Full diff: v0.1.5...v0.1.6
v0.1.5
A second lens over the same static analysis: modscan-audit maps a codebase's attack surface, and moddability reasons now carry stable IDs.
Added
- Security Lens (
modscan-audit) — a sibling command that maps a Python codebase's attack surface: where untrusted code or data can enter and execute (eval/exec,pickle/marshal/yamldeserialization,__reduce__,os.system/subprocess, dynamic loaders). Bandit-style catalog: stableMS-SEC-*ids rated by severity × confidence (separate from the moddability score), ranked most-dangerous-first. Offline, no LLM. Markdown or--json. Every report states prominently it is not a vulnerability scan — no taint, CVEs, or secrets, and an empty report is not a clean bill of health. Enumeration only; it does not trace reachability. - Stable IDs on moddability signals — each ranking reason carries a stable
MS-MOD-*id (presentation only; score and ranking unchanged).
Full diff: v0.1.4...v0.1.5
v0.1.4
Downstream polish: scaffolds you can verify, docs that point at them, and a gate comment that stops repeating itself.
Added
modscan scaffold --verify— after writing plugin skeletons, imports the target and confirms each base actually subclasses/instantiates (or, for hook/registration points, imports and is callable), reusing the layer-5 validator. Offline and free; exits non-zero if any point fails. Opt-in (executes the target's module code); the default scaffold path never imports the target.- Docs link to scaffolding, and explain the status badges. The plugin guide prints the exact
modscan scaffold "<id>"command per point; the index carries a legend for theverified/executed/compiled/generated/unverifiedbadges. Deterministic — no LLM.
Changed
- The breaking-change gate posts a sticky PR comment — it finds its prior comment by a hidden marker and updates it in place instead of stacking a new one on every re-push.
Full diff: v0.1.3...v0.1.4
v0.1.3
A free CI gate that fails a pull request removing or changing a library's extension points, plus detect polish.
Highlights
- Breaking-change gate GitHub Action (
Rinkia/modscan/breaking-change) — on a PR, diffsmodscan detectagainst the base branch, comments the diff, and fails when an extension point is removed or its category/kind changed. Protects a library's plugin/mod authors from silent breakage. No committed manifest, no LLM, no API key. Oneuses:line. modscan diffacceptsdetect --json(a flat list) as well as a manifest, so the gate runs off free detect output.scoreis never compared — re-ranking alone is not breaking.modscan detect --label— a clean header label instead of the scan path.- Fix: no absolute scan path leaks into detect ids (root-package symbols); JSON gains an explicit
modulefield. - Fix: the output directory is excluded from the scan (no self-scan on re-runs).
- Chore: GitHub Actions bumped off deprecated Node 20.
MODScan runs the gate on its own API — see .github/workflows/extension-api-gate.yml.
Full detail in CHANGELOG.md.
Install: `pip install modscan`
v0.1.2
Robustness for runs against real, imperfect codebases, plus cleaner output and a pipeline diagram in the README.
Highlights
- Pre-flight import probe — a run stops before any LLM call with a classified cause and a
pip installremediation when the target or its dependencies can't be imported, instead of grinding through to empty docs. - Classified drop reporting — extension points that fail validation are counted and labelled (missing dependency vs. other), in the CLI summary and an
index.md"Not documented" section, instead of vanishing silently. modscan detect: separate "Plugin registration points" section —entry_points-style loader sites are reported apart from the implement-this ranking and de-duplicated.- Run isolation — modules imported during validation are cleaned up, so consecutive runs in one process don't contaminate each other.
- Fix: the re-export ranking signal now fires when scanning a parent directory (a real checkout), not only when pointed straight at the package.
- Self-scan guard — warns when
--outis inside the scanned tree.
Full detail in CHANGELOG.md. See the new pipeline diagram in the README.
Install: `pip install modscan`
v0.1.1
Ships the offline, no-API-key surface and the measured ranking work.
Highlights
- `modscan detect` — rank a codebase's extension points with static analysis only: no LLM, no API key, no code execution. Markdown or `--json`.
- GitHub Action (`action.yml`) — ranked extension points in every PR's job summary, safe on untrusted PRs.
- MCP server (`pip install modscan[mcp]`, `modscan-mcp`) — the detector as a tool for AI clients (Claude Desktop, Cursor).
- Ranking benchmark (`benchmarks/`) — labelled ground truth over five real packages, `score.py` reporting recall@10, plus a CI guard against quietly bending the labels.
- Re-export + override-point signals, each judged on the benchmark (aggregate recall@10 4/12 → 8/12 on the original three targets), with honest generalisation gaps recorded.
- Fix: builtin `import` is no longer misranked as a plugin loader.
Full detail in CHANGELOG.md. See ROADMAP.md for what's next.
Install: `pip install modscan`
v0.1.0 — TypeScript support, breaking-change diffs, sandboxed validation
MODScan scans a codebase and generates everything you need to write plugins and mods for it — it finds the seams a modder can actually hook into, writes grounded documentation for them, and proves each generated example by loading it into the target.
Now on PyPI: pypi.org/project/modscan
pip install modscan[anthropic] # or [openai], [gemini], [typescript]
export ANTHROPIC_API_KEY=sk-ant-...
modscan ./your-project # -> modding-docs/ + extension-points.json
modscan scaffold "pkg.mod:Symbol" # -> a ready-to-edit plugin skeletonWant to look before spending anything? The deterministic layers need no API key
at all:
from modscan import parse_codebase, build_graph, detect_extension_points
cb = parse_codebase("./your-project")
for p in detect_extension_points(build_graph(cb), min_score=0.5)[:10]:
print(f"{p.score:.2f} {p.category} {p.seam.module}:{p.seam.name}")Marked a pre-release: this is a 0.x, the API can still move, and two known
limitations are documented below rather than hidden.
Everything since the MVP: more languages, more surfaces, safer execution, and a
substantial internal cleanup.
Added
- TypeScript/JavaScript front-end (experimental, via tree-sitter). Parses
.ts/.tsx/.js/.jsxinto the shared model, so the graph, detector and docs work
on JS/TS too.--language typescript|javascript; optional dependency
modscan[typescript]. Example execution-validation remains Python-only. - Pluggable language front-ends —
LanguageParserprotocol plus a registry,
so a new language plugs in without touching the graph, detector or generator. modscan diff— compare twoextension-points.jsonmanifests and report
removed / changed / added extension points. Exits non-zero on breaking
changes, so it can gate CI. A copy-paste workflow that comments the diff on a
pull request ships inexamples/ci/breaking-change.yml.modscan config— detect config/data-driven modding surfaces (manifest
files, drop-in directories) that the AST detector cannot see.modscan scaffold --all— generate plugin skeletons for every point in a
manifest in one command.- Subprocess sandbox (
--sandbox) — validate generated examples in an
isolated child process with a timeout, containing hangs and crashes when the
target is less trusted. - On-disk LLM response cache (
--cache-dir) — memoises responses by
provider, endpoint, model and prompt, making re-runs free and offline. - Native Gemini provider (
--provider gemini), alongside Anthropic and the
OpenAI-compatible adapter. - Concurrency for the LLM fan-out (
--concurrency, default 1). A run is
dominated by1 + points x (1..retries)sequential network round-trips; this
is where the wall-clock time is. Output is unaffected — results are collected
in input order and in-process validation is serialised. executedexample status — hook/registration examples are now executed,
not merely compile-checked, so bad imports and runtime errors are caught.- Continuous integration across Python 3.10–3.13, and an opt-in live smoke test
(MODSCAN_LIVE=1) that is the only test allowed to make a real API call. - Contributor documentation (
CONTRIBUTING.md) and README badges.
Changed
- Single source of truth for code execution. Loading a target, executing an
example, finding a concrete subclass and instantiating it previously existed
three times — in the validator, in the generator, and again as a Python string
inside the sandbox runner. It now lives once inmodscan/execution.py, and the
sandbox child imports that module instead of carrying a copy. docgensplit into a package (pipeline,examples,render,types).
The public API is unchanged.- Domain types moved to
models.py(Seam,ExtensionPoint), so layers no
longer import each other merely to name a type. Old import paths still resolve. - Example statuses are an enum (
models.ExampleStatus) instead of strings
re-spelled across four modules. It subclassesstr, so the JSON manifest is
byte-for-byte unchanged. - One filesystem convention —
SKIP_DIRS, source walking and slug
generation live inmodscan/fsutil.py. - Library-safe logging: a
NullHandleron the package root, plus debug logs
where failures were previously swallowed without a trace.
Fixed
- Crash on
typing.Protocolseams. Scanning a codebase that defines a
runtime_checkableProtocol with a data member aborted the run, because
issubclassraises for those and the call sat outside the guard. Found by
running MODScan on itself. - Divergent directory skipping.
SKIP_DIRSwas defined three times with
three different values, so the same tree produced different results depending
on which scanner asked —dist/was walked by the Python parser but skipped
elsewhere,.venv/was walked by the TypeScript one. Now unified. This is a
deliberate behavior change: build output and virtualenvs are consistently
skipped. - More dynamic-import calls detected —
import_string,load_entry_point,
iter_entry_points,load_module,find_specandget_loadernow register
as plugin-loader seams. Thanks to @AjibadeHassan
for the first external contribution (#18). - Silent point loss. Point selection relied on
zip()truncating against a
shorter validation list; the limit is now applied explicitly and the zip is
strict, so a mismatch fails loudly. javascriptexamples are written as.jswith ajavascriptcode fence
(previously.ts/typescript).- Cache keys include the provider and endpoint, so the same model and prompt
against two different backends can no longer collide. - Removed a dead import left behind by the language-front-end integration.
Known limitations
- Imported target modules are never unloaded, so a long-lived process
accumulates them and a second scan of a different tree using the same package
name reuses the first. Restoringsys.modulessafely needs its own change. - Generating docs into the tree being scanned makes the next run pick up its
own generated examples. Write the output elsewhere, or ignore the output
directory.
Full changelog: CHANGELOG.md · Compare: v0.0.1...v0.1.0
Contributions welcome — there are good first issues open, and CONTRIBUTING.md explains the layered architecture. Tests are framework-free and offline: no API key needed to hack on it.