IANUA-Broker
Local-first, offline-by-default security posture scanner for MCP / local-agent setups. Find exposed servers, plaintext secrets, over-broad tool scopes, and unpinned packages — then fix the highest-impact issues first.
Status: Stable (v1.x) — safe to run (read-only, offline by default), with
a stable CLI surface, JSON schema, and check ids covered by semver. CLI
command: mcpscan. License: Apache-2.0.
What it does
- Discovers MCP servers on the local machine via socket/process enumeration
(bind addresses classified by reachability — loopback / private-LAN /
public-routable / wildcard) plus a loopback probe of
/mcpand/sse. - Statically audits Claude-ecosystem (
.claude/settings.json,.mcp.json,claude_desktop_config.json), Cursor (~/.cursor/mcp.json,.cursor/mcp.json), Windsurf (~/.codeium/windsurf/mcp_config.json), Cline (VS CodeglobalStorage/…/cline_mcp_settings.json), VS Code (.vscode/mcp.json, usermcp.json), Zed (.zed/settings.json, user~/.config/zed/settings.json), and Continue (~/.continue/config.yaml,.continue/config.yaml— needs the[yaml]extra) agent configs — plus.env— for plaintext secrets, auto-approval flags, over-broad tool scopes, unpinned versions, reused credentials, and tool-poisoning signals. - Scores each server A–F across four dimensions (exposure, credential hygiene, tool-scope breadth, version pinning).
- Reports a prioritized, redacted, advise-only remediation in four
forms: terminal, a self-contained HTML file, stable JSON, and SARIF 2.1.0 for
GitHub code scanning — and can alert (
--emitto webhook/syslog/NDJSON). - Inventories (
mcpscan inventory) the machine's AI infrastructure as a classified, typed asset list — agent hosts, MCP servers, model servers, inference endpoints, LLM gateways, vector DBs — with per-asset evidence and confidence. - Validates continuously — a per-agent Trust Score (
mcpscan trust), framework mapping (mcpscan atlas), a signed baseline + drift gate (mcpscan baseline/diff), a named-human risk-acceptance ledger, validation-age staleness warnings, and OS-native scheduling (mcpscan schedule) turn a one-shot scan into an ongoing posture program. - Goes deeper, opt-in —
--onlineOSV dependency-vuln lookups,--inspect-token-stores(OAuth/session tokens at rest),--inspect-process-env(secrets in running agent processes),--inspect-telemetry(agent-host logging health),mcpscan selftest(catches a degraded scanner), and a signed detection data-pack refresh channel (mcpscan update-datapack).
Trust properties (by design)
- Localhost only —
scannever touches the LAN or third-party systems (mcpscan lanis a separate, signed-manifest-gated command). - Offline + zero egress by default — the network is contacted only under an
explicit opt-in:
--online(OSV dependency lookups) ormcpscan update-datapack; each one says so. - Reads nothing extra by default — deeper surfaces (token stores, running
process environments, host logging) are read only behind their
--inspect-*flag, which discloses what it touches. - Secrets never leak — redacted everywhere;
--show-secretsreveals only a masked/partial value, with a warning. - Advise-only by default — never writes to your config files unless you pass
--fix, which applies only safe, reversible tool-scope edits and backs up every file it touches first. - Fully stateless — writes only the report you explicitly ask for.
- Passes its own scan — exposes no port, ships no plaintext secret.
Install
pipx install ianua-broker # provides the `mcpscan` command
pipx install "ianua-broker[yaml]" # + audit Continue's config.yaml
pipx install "ianua-broker[crypto]" # + verify ed25519 LAN manifests / data-packs
The base install is stdlib-only (plus psutil). The optional [yaml] and
[crypto] extras enable Continue config auditing and library-based Ed25519
manifest verification, respectively.
Or from source:
git clone https://github.com/IRsoctierDT/IANUA-Broker.git
cd IANUA-Broker && pipx install .
Requires Python 3.11+ (macOS, Linux, Windows).
Usage
mcpscan scan # scan localhost + cwd project configs
mcpscan scan --root ~/project # scan a specific project root (repeatable)
mcpscan scan --json report.json # also write a stable JSON report (0600)
mcpscan scan --html report.html # also write a self-contained HTML report
mcpscan scan --sarif results.sarif # also write SARIF 2.1.0 for code scanning
mcpscan scan --fail-on critical # CI: exit non-zero only on Critical
mcpscan scan --online # opt-in OSV dep-vuln lookups (discloses egress)
mcpscan scan --emit webhook --emit-url … # emit findings/gate as an alert (opt-in)
mcpscan scan --inspect-token-stores # opt-in: OAuth/session tokens at rest
mcpscan scan --inspect-process-env # opt-in: secrets in running agent processes
mcpscan scan --inspect-telemetry # opt-in: agent-host logging health
mcpscan scan --show-secrets # reveal masked (first-2/last-2) values
mcpscan scan --fix # apply safe tool-scope fixes (backs up first)
mcpscan inventory # classified AI/MCP asset list (see below)
mcpscan atlas # findings mapped to security frameworks
mcpscan trust # per-agent Trust Score + risk relationships
mcpscan trust --min-grade B # CI: fail if any tool grades below B
mcpscan graph # cross-server AI attack-path graph (see below)
mcpscan graph --graph-format dot # Graphviz DOT export for visualization
mcpscan baseline --out base.json # snapshot current posture (digest-signed)
mcpscan diff --baseline base.json --fail-on-regression # drift gate for CI
mcpscan diff --baseline base.json --max-age-days 30 # + warn if the baseline is stale
mcpscan schedule --cadence daily # generate an OS-native scheduled scan+diff
mcpscan selftest # verify the scanner's own detections still fire
mcpscan update-datapack --pack p.json --signature p.sig --allowed-signers s # signed catalog refresh
mcpscan lan --manifest auth.toml ... # authorized network assessment (see below)
.mcpscan-accept.json in a scanned root lets a named human risk-accept a
specific tool-scope finding until a stated expiry — the finding still lowers the
grade but stops failing the gate until it lapses (then it re-arms, loudly).
Exit code is non-zero when a finding meets --fail-on (default: high), so it
drops straight into CI.
Example output
IANUA-Broker — overall posture: F
dimensions: credential=D, exposure=A, pinning=A, tool_scope=C
findings: 1 critical, 1 high, 2 medium
▶ ~/.mcp.json#weather [grade F]
[CRITICAL] Plaintext OpenAI API key in config
where: ~/.mcp.json
secret: [redacted len=37 sha256:c0cc596e]
fix: Remove the literal value from the file. Reference it from a
secret manager … and rotate the exposed credential.
[MEDIUM ] Server 'weather' runs an unpinned package via npx
fix: Pin the package to an exact version (e.g. npx some-pkg@1.2.3).
Fixing findings (--fix)
The tool is advise-only by default. --fix is the one explicit exception that
writes to your configs, and it stays deliberately conservative:
- Scope: removes over-broad tool-scope grants only — dangerous
(shell/exec-class) and wildcard entries from
permissions.allowand each server'sautoApprove, using the exact predicates the scanner flags with, so a fixed config re-scans clean. - Reversible: every modified file is copied to
<path>.mcpscan.bakbefore the edit, and the file's permissions are preserved. - Nothing invented: credential and pinning findings are not auto-fixed — a safe rewrite would need a new home for the secret or a specific version the tool can't know offline, so those stay manual (the report tells you what to do).
$ mcpscan scan --root . --fix
note: --fix modifies config files in place (backup written to <path>.mcpscan.bak) …
fixed ./.mcp.json (1 change(s); backup: ./.mcp.json.mcpscan.bak)
removed 'Bash(*)' from permissions.allow [SCOPE-DANGEROUS-ALLOW]
applied 1 fix(es). Re-run mcpscan to confirm.
AI/MCP asset inventory (mcpscan inventory)
Where scan judges posture, inventory answers what AI systems exist here —
it classifies what the scanner discovers (host configs, declared servers,
listening sockets) into a typed asset list: agent hosts, MCP servers, model
servers (Ollama, vLLM, LM Studio, llama.cpp), OpenAI-compatible inference
endpoints, LLM gateways (LiteLLM), and vector databases (Qdrant, Chroma,
Weaviate, Milvus). Three evidence tiers set the confidence: exact process name
or a product endpoint fingerprint (high), a generic OpenAI-compatible or
MCP transport surface (medium), a default-port hint alone (low).
$ mcpscan inventory
IANUA-Broker — inventory: 3 asset(s)
▶ MCP servers (1)
MCP server (HTTP transport) [medium confidence]
where: 127.0.0.1:40239
process: claude (pid 552)
evidence: responded on /mcp (HTTP 405)
▶ Model servers (1)
Ollama [high confidence]
where: 127.0.0.1:11434
process: ollama (pid 903)
evidence: process name 'ollama'
…
Inventory observes, never judges: it carries no severities, always exits 0,
and --json gives the stable machine-readable form. Fingerprinting stays inside
the trust boundary — loopback-only bare GETs (--no-probe disables even that),
response bodies are treated as hostile and never reach the output. Unrecognized
services are deliberately not listed: a plain web server is scan's exposure
concern, not an AI asset.
Agent trust analysis (mcpscan trust)
Where scan grades hygiene, trust asks what each agent tool is trusted to do
and access — and, crucially, which combinations make it a lateral-movement
risk. Every MCP server gets a Trust Score (0–100) across five factors —
secret access, tool privilege, autonomy (auto-approval), code provenance, and
network reach (a bind hint beyond loopback) — and the dangerous factor
combinations are surfaced as risk relationships that no single hygiene
check sees:
$ mcpscan trust
▶ 'db' [claude] Trust 25/100 (grade F)
· secret_access: holds 1 credential in its environment (+25 risk)
· tool_privilege: auto-approves 1 dangerous tool(s) (+25 risk)
· autonomy: auto-approves 1 tool(s) with no human in the loop (+15 risk)
· code_provenance: runs an unpinned / remotely-fetched package (+10 risk)
⚠ PRIVILEGED-SECRET-HOLDER — a single compromise leaks the secrets and the
power to use them.
⚠ AUTONOMOUS-PRIVILEGED — dangerous tools auto-approved, no human in the loop.
The relationships are the differentiator: PRIVILEGED-SECRET-HOLDER (secrets +
dangerous tools), AUTONOMOUS-PRIVILEGED (auto-approves dangerous tools),
AUTONOMOUS-SECRET-HOLDER, UNVETTED-PRIVILEGED (unpinned code + dangerous
tools), AUTONOMOUS-EXFIL-PATH (autonomy × privilege × secrets — an unattended
exfiltration path), EXPOSED-PRIVILEGED (network-reachable × dangerous tools),
and SHARED-CREDENTIAL (one secret spanning several tools — a cross-tool blast
radius). Scoring reuses the exact predicates scan trusts, so the two never
diverge. --min-grade makes it a CI gate; --json emits the full analysis; a
profile is secretless (a credential count, never a value). Read-only and
offline.
AI attack-path graph (mcpscan graph)
Where trust scores each server on its own, graph chains them: an AI
attack-path graph (not a network graph) that reasons about tool and trust
chaining — how an attacker who lands on an exposed surface pivots, via a shared
credential and a privileged/autonomous tool, to a high-value target.
$ mcpscan graph
IANUA-Broker — attack paths: 1 path(s) (1 critical, 0 high); overall grade F
[CRITICAL] exposed 'db' (wildcard / public bind) -> shared credential GITHUB_TOKEN
-> 'shell' (autonomous, dangerous tools) -> GitHub
why: a credential shared across servers lets the attacker pivot from the
exposed server to another that holds the same secret; 'shell'
auto-approves dangerous tools, so it acts with no human in the loop.
It composes over what the tool already collects — trust factors, reachability
tiers, shared-credential fingerprints, and the inventory — plus one safe
inference: a credential's key name (GITHUB_TOKEN, never its value) maps to
the target it unlocks. Path severity follows the entry's reachability
(wildcard/public → Critical, private-LAN → High), with a shared-credential
cross-server pivot escalating to Critical. --graph-format dot exports Graphviz
for visualization; --json emits the full node/edge/path model; --fail-on
makes it a CI gate. Pure, offline, read-only, and secretless (no raw value
reaches the terminal, JSON, or DOT).
Drift detection (mcpscan baseline / mcpscan diff)
Turn the one-shot scan into continuous posture. mcpscan baseline writes a
normalized, byte-stable snapshot of the current posture (findings, server
exposure, and the AI/MCP inventory) with an integrity digest; mcpscan diff
compares a fresh scan against it and reports what drifted — regressions
first:
$ mcpscan diff --baseline base.json --fail-on-regression
IANUA-Broker — drift: 4 change(s) (2 regression(s), 0 improvement(s))
+ [REGRESSION ] SCOPE-DANGEROUS-ALLOW — Dangerous tool auto-allowed: 'Bash(*)'
+ [REGRESSION ] PIN-UNPINNED — Server 'db' runs an unpinned package via npx
~ [REGRESSION ] socket://…:8000 exposure: local → exposed
The direction is the point: a new finding or a newly-exposed server is a
regression; a resolved finding or a server that stopped being exposed is an
improvement; new/removed assets are informational. A disappearing security
control surfaces as a new finding (the check that the control was present now
fires). --fail-on-regression exits non-zero only on regressions, so
diff drops into CI to block posture backsliding — commit a signed baseline,
then diff every change against it. The baseline's digest is re-verified on load,
so an edited or corrupted baseline is refused rather than trusted. --json
emits the full machine-readable drift; --no-inventory snapshots posture only.
Framework mapping (mcpscan atlas)
atlas renders the same findings scan produces, each annotated with its
security-framework citations — MITRE ATT&CK, MITRE ATLAS, OWASP LLM
Top 10, NIST AI RMF (function level), and CIS Controls v8 (control
level) — so a finding drops straight into an assessment report or a GRC tool.
$ mcpscan atlas
[CRITICAL] CRED-PLAINTEXT: Plaintext High-entropy secret in config
↳ MITRE ATT&CK T1552.001 — Unsecured Credentials: Credentials In Files
↳ MITRE ATLAS AML.T0055 — Unsecured Credentials
↳ OWASP LLM Top 10 LLM02 — Sensitive Information Disclosure
↳ NIST AI RMF GOVERN — Govern function
↳ CIS Controls v8 Control 3 — Data Protection
--matrix prints the full static check-id → framework table without scanning;
--json emits mapped findings plus the matrix. The mapping table is
deliberately conservative — a citation appears only where the technique/control
match is direct, NIST AI RMF stays at function level and CIS at control level —
and it lives in one auditable data file
(src/mcpscan/atlas/model.py), with CI gating
that every check id the scanner can emit has a mapping and no mapping outlives
its check. Exit-code semantics match scan (--fail-on).
GitHub code scanning (SARIF)
--sarif writes a SARIF 2.1.0 log that GitHub ingests as code-scanning alerts on
the Security tab, with per-finding severity (security-severity) and stable
fingerprints so alerts track across commits. Paths inside the scanned repo are
emitted repo-relative so alerts annotate the offending line; secrets are never
present (only the redacted fingerprint). SARIF covers config-file findings;
running-socket exposure (no source file) stays in the terminal/JSON/HTML views.
Drop this into a workflow:
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install ianua-broker
- run: mcpscan scan --sarif results.sarif --fail-on critical
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v4
with: { sarif_file: results.sarif }
This repo dogfoods it in .github/workflows/mcpscan.yml.
Authorized network assessment (mcpscan lan)
mcpscan scan is localhost-only. mcpscan lan is a separate, gated command
for assessing MCP exposure on hosts you are authorized to test — and it is
inert without a signed authorization manifest. Governing principle:
discovery never converts into authority. It is exposure-only (never reads a
remote config), private-address by default, and bounded by immutable budgets.
mcpscan lan --manifest auth.toml \
--signature auth.toml.sig \
--allowed-signers allowed_signers \
--invoker human \
--dry-run # verify + print the plan; send no packets
The manifest is a signed TOML file naming exact targets, ports, operator, and expiry:
authorization_id = "ENG-2026-0710"
operator = "you@example.com"
expires_at = "2026-07-10T23:59:59Z"
targets = ["192.168.10.20/32"] # exact hosts / /32 (a human may use a capped CIDR)
ports = [3000, 8000]
Sign it with your SSH key (ssh-keygen -Y sign -n mcpscan-lan -f key auth.toml).
--invoker agent gets tighter budgets and exact-hosts-only. Public targets are
refused unless named in an --enterprise-policy file. --json and --sarif
both work: because a LAN finding's location is a network endpoint (not a source
file), --sarif emits it as a SARIF logical location
(kind: resource, fullyQualifiedName: lan://host:port) — standards-valid for
generic SARIF and SIEM/audit consumers, not GitHub code scanning (which
needs a checkout file to raise an alert). No synthetic file path is ever
invented; see ADR-16. Step-by-step:
docs/LAN_OPERATOR_GUIDE.md; full design and threat
model: docs/proposals/LAN_SCANNING.md.
Documentation
| Doc | What it is |
|---|---|
| docs/SPEC.md | Full product & technical specification (testable requirements, scoring rubric, threat model, DoD). |
| docs/DECISIONS.md | 15 architecture decision records. |
| docs/ARCHITECTURE.md | Component model, dependency direction, trust boundaries. |
| docs/BACKLOG.md | Sprint-tagged tickets + requirement→ticket traceability. |
| docs/SECURITY_SIGNOFF.md | Threat-model verification matrix (security sign-off). |
| docs/agents/ | MCP Sentinel agent suite — governed agent roles, registry, and operating model. |
| SECURITY.md · CONTRIBUTING.md | Reporting policy · contributor guide. |
Status & roadmap
Released on PyPI as ianua-broker
(the mcpscan command) — stable and production-ready, behind a green CI gate
(ruff, mypy --strict, bandit, pytest with a 90% branch-coverage floor, on
macOS/Linux/Windows × Python 3.11–3.13), with SBOM + checksums on every release.
It ships seven host adapters (Claude, Cursor, Windsurf, Cline, VS Code, Zed,
Continue), SARIF 2.1.0 + a GitHub code-scanning workflow, opt-in --fix,
mcpscan lan (authorized, signed-manifest network assessment), and a
dogfood harness that gates every check against a
clean+messy corpus across all hosts (0 false positives / 0 false negatives, run
in CI).
The CLI surface, JSON report schema, and check ids are covered by semver: breaking changes to any of them mean a major version bump.
Since 1.0, three hardening waves have landed on top of the platform tiers in
docs/proposals/VISION.md (inventory, atlas,
trust, and baseline/diff drift): continuous-validation foundations
(validation-age staleness, a named-human risk-acceptance ledger, drift-cause
tags, reused-credential detection); detection reach & quiet-read surfaces
(the --emit alert layer, mcpscan schedule, token-store and running-process
credential inspection, and an autonomous-exfiltration trust composite); and
hardening & extensibility (reachability tiering, --online dependency-vuln
lookups, tool-integrity heuristics, agent-host telemetry checks, mcpscan selftest, and a signed detection data-pack refresh channel). graph
(Tier 3 — the cross-server AI attack-path graph) has now landed, completing
the platform tiers. Next: real-lab dogfooding (stakeholder configs + a
pfSense/Suricata network lab).
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ianua_broker-1.4.0.tar.gz.
File metadata
- Download URL: ianua_broker-1.4.0.tar.gz
- Upload date:
- Size: 340.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7f66106e3e89c4d6b8c2b85809a4e80f7692d9fea45ad6a011b99d824e0c68b
|
|
| MD5 |
593b13dd8e5c0d588d0f50ad53b5077a
|
|
| BLAKE2b-256 |
5491442724f325bf64de674808d92d58fd0288104f3c6556bf88309301361062
|
Provenance
The following attestation bundles were made for ianua_broker-1.4.0.tar.gz:
Publisher:
release-please.yml on IRsoctierDT/IANUA-Broker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ianua_broker-1.4.0.tar.gz -
Subject digest:
d7f66106e3e89c4d6b8c2b85809a4e80f7692d9fea45ad6a011b99d824e0c68b - Sigstore transparency entry: 2447390961
- Sigstore integration time:
-
Permalink:
IRsoctierDT/IANUA-Broker@4456929b847deab0da90c29ed6676a9572595a87 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/IRsoctierDT
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@4456929b847deab0da90c29ed6676a9572595a87 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ianua_broker-1.4.0-py3-none-any.whl.
File metadata
- Download URL: ianua_broker-1.4.0-py3-none-any.whl
- Upload date:
- Size: 193.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3014f4f66d30b17f463ba445c01d0872cecd20e38bc8452f3d9ab5a44630a064
|
|
| MD5 |
c59175d0f701e61c3d9223fb69aa536b
|
|
| BLAKE2b-256 |
48021831d634a346efafeb6fbf160cbdad6deec3a2e3f44ae3da50c54ecc9427
|
Provenance
The following attestation bundles were made for ianua_broker-1.4.0-py3-none-any.whl:
Publisher:
release-please.yml on IRsoctierDT/IANUA-Broker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ianua_broker-1.4.0-py3-none-any.whl -
Subject digest:
3014f4f66d30b17f463ba445c01d0872cecd20e38bc8452f3d9ab5a44630a064 - Sigstore transparency entry: 2447391321
- Sigstore integration time:
-
Permalink:
IRsoctierDT/IANUA-Broker@4456929b847deab0da90c29ed6676a9572595a87 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/IRsoctierDT
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@4456929b847deab0da90c29ed6676a9572595a87 -
Trigger Event:
push
-
Statement type: