Secops Toolkit MCP
by glatinone
README.md
# SecOps Toolkit MCP
[](https://github.com/glatinone/secops-toolkit-mcp/actions/workflows/ci.yml)
[](https://github.com/glatinone/secops-toolkit-mcp/releases/latest)
[](LICENSE)
A small, dependency-light [Model Context Protocol](https://modelcontextprotocol.io)
server that gives an AI assistant a set of **defensive security helpers** for
working with logs, threat-intel notes, and network data — all running locally,
with no API keys and no outbound network calls.
Built with [FastMCP](https://github.com/jlowin/fastmcp).
## Tools
| Tool | What it does |
| --- | --- |
| `extract_iocs` | Pull IPs, URLs, domains, MD5/SHA1/SHA256 hashes, and CVE IDs out of free-form text. Handles defanged input (`1.2.3[.]4`, `hxxp://`). |
| `defang_ioc` | Make an indicator safe to paste: `1.2.3.4` → `1.2.3[.]4`. |
| `refang_ioc` | Reverse a defanged indicator back to its real form. |
| `hash_text` | Hash a string with md5 / sha1 / sha256 / sha512. |
| `password_entropy` | Estimate password strength in bits of entropy. |
| `cidr_info` | Describe a CIDR network: netmask, host range, size, privacy. |
| `ip_in_cidr` | Check whether an IP falls inside a CIDR range. |
| `scan_repo_root` | Check a repo's top-level directory for files that shadow common dev command names (`git.exe`, `node.exe`, etc.), and the whole tree for symlinks that resolve outside the repo, before you open it in an agentic coding tool. |
| `assess_shell_command` | Assess a shell command for constructs (quote fragmentation, command substitution, IFS tricks, encoded pipelines, setuid grants, env-var poisoning via `export`/`typeset`/`declare`) that look benign to naive string matching but execute something else once a shell expands them. |
> These are **defensive / analysis** utilities — parsing, hashing, and network
> math. They don't scan, attack, or reach out to any host.
## Quickstart
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
```bash
git clone https://github.com/glatinone/secops-toolkit-mcp.git
cd secops-toolkit-mcp
uv sync
uv run secops-toolkit-mcp # starts the server over stdio
```
That's it, the server is now running and waiting for an MCP client to connect
over stdio. Wire it into a client (below), or call the underlying functions
directly in Python (see Examples).
## Use it from an MCP client
Add this to your client's MCP config (e.g. Claude Desktop's
`claude_desktop_config.json`). Point `--directory` at where you cloned the repo:
```json
{
"mcpServers": {
"secops-toolkit": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/secops-toolkit-mcp", "secops-toolkit-mcp"]
}
}
}
```
Then ask your assistant things like *"extract the IOCs from this alert"* or
*"is 10.0.4.20 inside 10.0.0.0/16?"* and it will call these tools.
## Standalone CLI: `secops-scan-repo`
`scan_repo_root` is also available outside an MCP client, as its own console
script, for the exact moment it matters most: right after you clone or
download a repo and before you open it in an agentic coding tool.
```bash
uv run secops-scan-repo /path/to/a/freshly-cloned-repo
# secops-scan-repo: /path/to/a/freshly-cloned-repo (4 top-level file(s), 1 symlink(s) scanned)
#
# [CRITICAL] git.exe shadows the 'git' command
# [CRITICAL] vendor_link is a symlink resolving outside the repo root, to /home/dev/.ssh/authorized_keys
```
Exits `0` on a clean directory, `1` if a finding is at or above
`--min-severity` (default `medium`, i.e. any finding), `2` on a bad path.
`-f/--format json` gives the same structure `scan_repo_root` returns, for
scripting. This makes it a one-liner in a pre-clone git hook:
```bash
#!/bin/sh
# .git/hooks/post-checkout (or a wrapper your clone script calls)
secops-scan-repo "$(git rev-parse --show-toplevel)" || {
echo "secops-scan-repo: refusing to continue, see findings above" >&2
exit 1
}
```
If installed system-wide (`uv tool install .` or `pip install .`), drop the
`uv run` prefix and call `secops-scan-repo` directly.
## Examples
The tools are plain functions in [`core.py`](src/secops_toolkit_mcp/core.py), so
you can call them directly (e.g. in a REPL or a script) without going through
an MCP client:
```python
from secops_toolkit_mcp.core import extract_iocs, defang_ioc, hash_text, password_entropy, cidr_info, ip_in_cidr, assess_shell_command
extract_iocs("Reached out to 1.2.3[.]4 and hxxp://evil.example.com, hash 5d41402abc4b2a76b9719d911017c592, see CVE-2024-1234")
# {'md5': ['5d41402abc4b2a76b9719d911017c592'], 'ipv4': ['1.2.3.4'],
# 'url': ['http://evil.example.com'], 'domain': ['evil.example.com'],
# 'cve': ['CVE-2024-1234']}
defang_ioc("http://1.2.3.4/payload")
# 'hxxp[://]1[.]2[.]3[.]4/payload'
hash_text("hello world")
# {'algorithm': 'sha256', 'hex_digest': 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'}
password_entropy("Tr0ub4dor&3")
# {'length': 11, 'charset_size': 94, 'entropy_bits': 72.1, 'strength': 'strong'}
cidr_info("10.0.0.0/24")
# {'network': '10.0.0.0', 'netmask': '255.255.255.0', 'prefix_length': 24,
# 'version': 4, 'num_addresses': 256, 'is_private': True,
# 'first_host': '10.0.0.1', 'last_host': '10.0.0.254', 'broadcast': '10.0.0.255'}
ip_in_cidr("10.0.0.5", "10.0.0.0/24")
# True
scan_repo_root("/path/to/a/freshly-cloned-repo")
# {'path': '/path/to/a/freshly-cloned-repo', 'entries_scanned': 4,
# 'symlinks_scanned': 1, 'clean': False, 'findings': [
# {'kind': 'shadowed_name', 'filename': 'git.exe', 'shadows': 'git',
# 'severity': 'critical'},
# {'kind': 'symlink_escape', 'path': 'vendor_link',
# 'resolves_to': '/home/dev/.ssh/authorized_keys', 'severity': 'critical'}
# ]}
assess_shell_command("r'm' -rf /")
# {'command': "r'm' -rf /", 'risk': 'dangerous', 'logical_commands': [
# {'raw_segment': "r'm' -rf /", 'tokens': ['rm', '-rf', '/'],
# 'effective_command': 'rm -rf /',
# 'findings': ['bypassed_raw_pattern_match', 'denylisted_pattern'],
# 'raw_denylist_hits': [], 'normalized_denylist_hits': ['recursive_root_delete'],
# 'risk': 'dangerous'}
# ], 'substitutions': [], 'pipe_findings': [], 'bypassed_raw_pattern_match': True}
# Note raw_denylist_hits is empty: a filter matching the raw text "r'm' -rf /"
# finds nothing. normalized_denylist_hits catches it because the tokens are
# what a real shell would actually run.
assess_shell_command("echo cm0gLXJmIC8= | base64 -d | sh")
# {'command': 'echo cm0gLXJmIC8= | base64 -d | sh', 'risk': 'dangerous',
# 'logical_commands': [
# {'raw_segment': 'echo cm0gLXJmIC8=', 'tokens': ['echo', 'cm0gLXJmIC8='],
# 'effective_command': 'echo cm0gLXJmIC8=', 'findings': [], 'risk': 'safe', ...},
# {'raw_segment': 'base64 -d', 'tokens': ['base64', '-d'],
# 'effective_command': 'base64 -d',
# 'findings': ['pipe_decoded_content_to_interpreter'], 'risk': 'dangerous', ...},
# {'raw_segment': 'sh', 'tokens': ['sh'], 'effective_command': 'sh',
# 'findings': ['pipe_decoded_content_to_interpreter'], 'risk': 'dangerous', ...}
# ],
# 'pipe_findings': [{'type': 'pipe_decoded_content_to_interpreter',
# 'segments': ['base64 -d', 'sh']}], ...}
# The base64 blob decodes to "rm -rf /" -- decoding it is what turns opaque
# text into an executable payload, the same "danger only visible after a
# transform the filter doesn't model" shape as piping a fetch into a shell.
assess_shell_command("cd ~/.ssh && cat authorized_keys")
# {'command': 'cd ~/.ssh && cat authorized_keys', 'risk': 'dangerous',
# 'logical_commands': [
# {'raw_segment': 'cd ~/.ssh', 'tokens': ['cd', '~/.ssh'],
# 'findings': ['cd_into_sensitive_directory'], 'risk': 'dangerous', ...},
# {'raw_segment': 'cat authorized_keys', 'tokens': ['cat', 'authorized_keys'],
# 'findings': [], 'risk': 'safe', ...}
# ], 'cd_findings': [{'type': 'cd_into_sensitive_directory',
# 'segments': ['cd ~/.ssh', 'cat authorized_keys']}], ...}
# A bare "cd ~/.ssh" alone would be safe -- nothing acts on it yet. Chained
# with a further command, the second segment's own relative-path filename
# ("authorized_keys" alone, no path prefix) would slip past every other
# check here, since none of them know the effective working directory changed.
assess_shell_command("export PAGER='open -a Calculator'")
# {'command': "export PAGER='open -a Calculator'", 'risk': 'dangerous',
# 'logical_commands': [
# {'raw_segment': "export PAGER='open -a Calculator'",
# 'tokens': ['export', 'PAGER=open -a Calculator'],
# 'findings': ['denylisted_pattern'],
# 'normalized_denylist_hits': ['env_poisoning_assignment'],
# 'risk': 'dangerous', ...}
# ], ...}
# Flagged even with nothing chained after it -- unlike "cd ~/.ssh", a
# poisoned PAGER persists in the calling agent's shell session and can be
# triggered by a later, separately-approved command ("git branch", "man ls")
# this function never sees, since each call only assesses one command string.
assess_shell_command("typeset -i ${(e):-'$(open -a Calculator)'}")
# {'command': "typeset -i ${(e):-'$(open -a Calculator)'}", 'risk': 'suspicious',
# 'logical_commands': [
# {'raw_segment': "typeset -i ${(e):-'$(open -a Calculator)'}",
# 'tokens': ['typeset', '-i', '${(e):-$(open -a Calculator)}'],
# 'effective_command': 'typeset -i ${(e):-$(open -a Calculator)}',
# 'findings': ['zsh_forced_parameter_expansion'],
# 'raw_denylist_hits': [], 'normalized_denylist_hits': [],
# 'risk': 'suspicious'}
# ], 'substitutions': [], 'pipe_findings': [], 'bypassed_raw_pattern_match': False}
# Flagged "suspicious", not "dangerous": zsh's (e) flag forces evaluation of the
# default value even though it's single-quoted, so the embedded $(open -a
# Calculator) runs anyway -- but (e) also has legitimate non-malicious uses,
# so this function can't claim certainty from the text alone.
```
From an MCP client, the same calls happen through natural language, for
example asking *"hash this string with sha256"* or *"describe the network
10.0.0.0/24"*.
## Why `scan_repo_root` exists
Mindgard disclosed (2026-07-15) that Cursor, GitHub Copilot CLI, Gemini CLI,
and Codex all resolve an unqualified `git` command on startup. Windows checks
the current working directory before `PATH`, so a cloned repo that ships a
file literally named `git.exe` at its root runs that file instead of the real
Git — before any workspace-trust prompt appears. As of this writing none of
the four vendors has shipped a fix.
`scan_repo_root` closes this independent of which tool eventually opens the
repo: point it at a directory before you open it (or wire it into a
pre-clone/pre-open hook) and it flags any top-level file whose name shadows a
commonly unqualified-invoked command (`git`, `node`, `npm`, `python`, `bash`,
`docker`, and similar), tiered by how directly the shape has been confirmed
exploitable:
- **critical** — `git`, the name Mindgard's disclosure confirmed end-to-end.
- **high** — shells and interpreters (`node`, `npm`, `npx`, `python`, `bash`,
`cmd`, `powershell`, ...) that agentic tools commonly invoke unqualified.
- **medium** — other common dev tools (`docker`, `make`, `curl`, `ssh`, `gh`,
...) plausibly invoked the same way but not confirmed by the disclosure.
Only the top-level directory is checked, matching how Windows's own
unqualified-command search actually works (current directory, then `PATH` —
it does not recurse into subdirectories).
### Symlink-escape check
Wiz's "GhostApproval" (Amazon Q, Cursor) and Cursor's "DuneSlide"
(CVE-2026-50548/50549, CVSS 9.3-9.8) disclosures are a second, unrelated
masquerading shape: an approval dialog or sandbox check displays a decoy
path while a symlink silently redirects the actual write target outside the
trusted directory — up to and including `~/.ssh/authorized_keys`. Both are
zero-click once triggered by a prompt-injected agent action.
`scan_repo_root` also walks the whole repo tree (not just the top level —
a planted symlink can sit anywhere a tool later writes through it) and
flags any symlink whose resolved target lies outside the scanned root:
- **critical** — the resolved target hits a known sensitive path (an
`.ssh`/`.aws`/`.gnupg`/`.docker`/`.kube`/`.azure` directory, or a private
key/credential filename like `authorized_keys`, `id_rsa`, `.npmrc`,
`.git-credentials`).
- **high** — resolves outside the root to anywhere else.
A symlinked directory's own contents are never walked into (only the
symlink entry itself is inspected), so this can't be tricked into
recursing outside the intended scan boundary.
## Why `assess_shell_command` exists
GuardFall (2026-06) tested 11 popular open-source AI coding agents (Aider,
Cline, Goose, Plandex, and others — roughly 548,000 combined GitHub stars)
and found that 10 of them run a command-safety guard which inspects the raw
string a model wrote, then hands that same string to a real shell. The shell
rewrites the string before anything executes: quotes get removed, backslash
escapes get resolved, `$(...)`/backtick command substitutions run, and
variables (including `$IFS`, the field separator, which defaults to
whitespace) get expanded. A guard that pattern-matches the raw text has
already lost by the time any of that happens. `r'm' -rf /` doesn't look like
`rm -rf /` to a regex, but a shell dequotes and concatenates it into exactly
that.
`assess_shell_command` closes that gap by tokenizing with the same
POSIX quote-removal rules a real shell uses (`shlex`), so the *normalized*
command is what gets checked, not the raw string. It also:
- Extracts `$(...)`/backtick command substitutions and recursively assesses
the command hidden inside them.
- Flags unquoted variable expansion, `$IFS`-based space-substitution tricks,
ANSI-C (`$'...'`) quoting, and unbalanced quotes — constructs whose real
effect can't be determined from the text alone.
- Flags a fetch piped straight into an interpreter (`curl ... | sh`), or an
encoded payload decoded and piped straight into one (`base64 -d | sh`,
`xxd -r | sh`) — patterns that give you no chance to inspect what you're
about to run.
- Flags setuid grants (`chmod +s`, `chmod 4755`, `install -m 4755`) and
overwriting a credential file in place (`sed -i`/`tee` targeting
`~/.aws/credentials`, `~/.ssh/authorized_keys`, and similar).
- Flags a symlink whose target *or* link name lands on a sensitive path
(`ln -s ~/.ssh/id_rsa ./notes.txt`), and a `cd` into a sensitive directory
chained with a further command that would act on relative paths inside it
(`cd ~/.aws && cat credentials`) — the shell-level shape of Cursor's
DuneSlide disclosure (see below).
- Flags `export`/`typeset`/`declare`/`readonly`/`local` assigning a
behavior-redirecting variable (`PAGER`, `BROWSER`, `PYTHONWARNINGS`,
`PERL5OPT`), and zsh's zero-click `${(e):-...}` forced-parameter-expansion
construct — Cursor's Terminal Tool Allowlist Bypass disclosure (see below).
It never executes the command, or any part of it, at any point — this is
static text analysis, not a sandbox. Returns `risk` (`safe`, `suspicious`, or
`dangerous`) plus `bypassed_raw_pattern_match`: concrete, per-call evidence
that a normalized check caught something a raw-text filter would have missed.
### Validated against GuardFall's disclosed bypass corpus
The bypass classes above aren't a guess at what GuardFall's disclosure might
cover — they were checked directly against GuardFall's own published
write-ups (not a summary of them). Confirmed already caught: quote
fragmentation/merge (`r''m`, `r'm' -rf /`), `$IFS`-as-space substitution,
command substitution used as an argument, backtick substitution, ANSI-C
quoting. Two real gaps found and closed in this pass: the encoded-pipeline
class (`echo <base64> | base64 -d | sh`) and several "alternative destructive
utility" shapes the denylist didn't cover (setuid grants, in-place credential
file overwrites, and `$HOME`/`~`-relative sensitive paths like
`rm -rf "$HOME/.aws/credentials"`, GuardFall's own cited real-world example).
One limitation is deliberate, not missed: a command substitution that
computes the *command name itself* (`$(echo rm) -rf /`) can only be flagged
`suspicious`, not `dangerous`, without actually running the substitution to
see what it produces — and running untrusted code to decide whether it's
safe to run defeats the point. See `tests/test_core.py`'s "GuardFall
disclosed corpus" section for the full payload-by-payload results.
### Closing DuneSlide's shell-level shape
Cursor's DuneSlide disclosure (CVE-2026-50548/50549, see the symlink-escape
section above) is two write-sandbox escapes: an agent-controlled
`working_directory` parameter that silently expands the writable-path
allowlist, and a symlink whose target the sandbox trusts instead of
rejecting when canonicalization fails. `scan_repo_root`'s symlink-escape
check already finds a symlink *planted* in a repo before it's opened —
but neither Cursor bug is about a pre-existing file; both are about what
the agent's *own* command does mid-session, which is `assess_shell_command`'s
job, not `scan_repo_root`'s.
Checked structurally against parsed tokens, the same way as the `rm`
sensitive-target check above (`ln`'s and `cd`'s flags vary too much for a
single regex), reusing the same sensitive dir/file name list:
- **`ln -s`/`ln --symbolic`**, when its target *or* its link name hits a
known-sensitive path (`.ssh`/`.aws`/`.gnupg`/`.docker`/`.kube`/`.azure`, or
a credential filename like `authorized_keys`/`id_rsa`) — either side can
be the sensitive one, since the attack's whole point is to make one side
look innocent.
- **`cd` into a known-sensitive directory**, only when chained (`&&`, `;`,
a pipe, or a newline) with a further command — a bare `cd ~/.ssh` with
nothing acting on it afterward hasn't done anything yet, the same
"danger is in the combination" posture `pipe_findings` already uses for
fetch-piped-to-interpreter.
A plain hard link (`ln` with no `-s`) is a structurally different, lower-risk
operation and stays out of scope. Both checks tokenize with the same quote-
removal and `$IFS`-expansion rules as everything else in this function, so
`l'n' -s ~/.ssh/authorized_keys ./x` and `cd$IFS~/.aws && cat credentials`
are caught too, not just the unobfuscated form.
### Closing CVE-2026-22708's env-poisoning shape
Cursor's Terminal Tool Allowlist Bypass (CVE-2026-22708, fixed in 2.3) is a
third, distinct bypass mechanism: a command allowlist validates *which
external binary runs*, but shell builtins aren't external binaries, so
`export`, `typeset`, `declare`, `readonly`, and `local` execute unconditionally
regardless of what's on the allowlist. An attacker (via prompt injection) uses
one of them to poison an environment variable a later, separately-approved
command reads — `PAGER` hijacks `git branch`/`man ls`, `BROWSER` hijacks a
URL-opening command, `PYTHONWARNINGS`/`PERL5OPT` chain into arbitrary
Python/Perl execution the next time those interpreters run. None of this
requires ever executing a disallowed command string.
`assess_shell_command` flags an `export`/`typeset`/`declare`/`readonly`/`local`
invocation that assigns one of these variables as `dangerous` on its own —
deliberately *not* requiring a chained follow-up command in the same string,
unlike the DuneSlide `cd` check above. A poisoned `PAGER` persists in the
calling agent's shell session; the command that actually gets hijacked is
usually issued in a later, separate tool call this function has no visibility
into, so the poisoning step itself is what has to be caught:
```python
assess_shell_command("export PAGER='open -a Calculator'")
# risk: dangerous — flagged even though nothing in *this* command string
# reads PAGER back yet.
```
The disclosure's PoC also included a zero-click variant with no `export` step
at all: zsh's `${(e):-...}` parameter-expansion flag forces evaluation of its
default value even when that value is single-quoted —
`typeset -i ${(e):-'$(open -a Calculator)'}` runs the embedded command despite
the quotes that would normally disable expansion, bypassing the same
single-quote assumption this function's own command-substitution extraction
relies on elsewhere. Flagged as `zsh_forced_parameter_expansion`, a construct
whose real effect can't be determined from the text alone — the same honest
treatment as `ifs_manipulation`/`ansi_c_quoting`, not overclaimed as
`dangerous` on its own since `(e)` also has legitimate, non-malicious uses.
Deliberately out of scope: `unset` and `set`, which the same disclosure also
names as bypassing the allowlist, but neither can carry a `VAR=value`
assignment the way `export`/`typeset`/`declare`/`readonly`/`local` do —
`unset` only removes a variable, and `set` (without `-o`) manipulates
positional parameters, not the environment. And `LD_PRELOAD`, a well-known
dangerous variable elsewhere, isn't in this list since it wasn't part of this
disclosure's own confirmed PoC payloads — added only if a future disclosure
or real-world case demonstrates it.
## Run with Docker
The included `Dockerfile` builds a container that launches the server over
stdio, so an MCP client can run it without a local Python/uv install:
```bash
docker build -t secops-toolkit-mcp .
docker run -i --rm secops-toolkit-mcp
```
To use it from an MCP client, point the client's config at `docker run`
instead of `uv run`:
```json
{
"mcpServers": {
"secops-toolkit": {
"command": "docker",
"args": ["run", "-i", "--rm", "secops-toolkit-mcp"]
}
}
}
```
## Troubleshooting
- **Client shows the server as disconnected / no tools listed.** MCP clients
talk to this server over stdio, so anything else printed to stdout will
break the protocol. Confirm you're running `uv run secops-toolkit-mcp` (or
the Docker command above) exactly, not piping it through another wrapper
that adds its own output.
- **`--directory` path errors in the client config.** The path must be
absolute and point at the cloned repo root (the folder containing
`pyproject.toml`), not the `src/` directory.
- **`ValueError` from `hash_text`, `cidr_info`, or `ip_in_cidr`.** These raise
on bad input (unsupported hash algorithm, malformed IP/CIDR) with a message
naming the invalid value, by design, rather than failing silently.
- **Python version errors during `uv sync`.** The project requires Python
3.11+ (see `.python-version`); `uv` will fetch a matching interpreter
automatically if one isn't already installed.
## Development
```bash
uv sync # install deps (incl. dev)
uv run pytest # run the test suite
```
The logic lives in [`core.py`](src/secops_toolkit_mcp/core.py) as plain,
testable functions; [`server.py`](src/secops_toolkit_mcp/server.py) is a thin
layer that exposes them as MCP tools.
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow for adding a new
tool, and [CHANGELOG.md](CHANGELOG.md) for release history.
## Roadmap
- [x] Initial tool set: IOC extraction, defang/refang, hashing, password
entropy, CIDR math (v0.1.0)
- [x] CI on Python 3.11 to 3.13, CHANGELOG (v0.2.0)
- [x] `scan_repo_root`, a pre-clone/pre-open check for binaries that shadow
common dev command names (v0.3.0), closing the Mindgard 2026-07-15
unqualified-`git` disclosure
- [x] `assess_shell_command`, a shell command safety check that assesses
what a shell actually runs rather than the raw string a model wrote
(v0.4.0), closing the GuardFall 2026-06 bypass class
- [x] `secops-scan-repo`, a standalone CLI for `scan_repo_root` (v0.5.0), so
it can run in a pre-clone git hook or CI step without an MCP client
- [x] `scan_repo_root` symlink-escape check (v0.6.0): flags a symlink
anywhere in the repo tree whose resolved target lies outside the repo
root, closing the GhostApproval/DuneSlide hidden-write-target pattern
- [x] `assess_shell_command` validated directly against GuardFall's disclosed
bypass corpus (v0.7.0), not just a research digest of it — closed two real
gaps found this way: encoded pipelines (`base64 -d | sh`, `xxd -r | sh`)
and alternative destructive-utility shapes (setuid grants, in-place
credential-file overwrites, `$HOME`/`~`-relative sensitive rm targets)
- [x] `assess_shell_command` widened for Cursor's DuneSlide disclosure
(v0.8.0): flags an agent-issued `ln -s`/`ln --symbolic` targeting (or
named after) a sensitive path, and a `cd` into a sensitive directory
chained with a further command — the shell-level shape of both
CVE-2026-50548 (`working_directory` abuse) and CVE-2026-50549 (symlink
canonicalization bypass)
- [x] `assess_shell_command` widened for Cursor's Terminal Tool Allowlist
Bypass (v0.9.0, CVE-2026-22708): flags `export`/`typeset`/`declare`/
`readonly`/`local` assigning a behavior-redirecting variable (`PAGER`,
`BROWSER`, `PYTHONWARNINGS`, `PERL5OPT`) that a later, separately-approved
command would then respect — a structurally distinct bypass from the
GuardFall and DuneSlide shapes above, since it attacks what a trusted
command's *environment* inherits, not what command string runs. Also
flags zsh's zero-click `${(e):-...}` forced-parameter-expansion construct
- [ ] Widen `scan_repo_root`'s shadowed-name/extension coverage, its
sensitive-target symlink list, and `assess_shell_command`'s denylist
patterns as real-world use surfaces gaps; all three are intentionally
high-signal, not exhaustive (see the module comments in `core.py`)
- [ ] PyPI packaging, once the same pattern is proven end to end on
[mcpscan](https://github.com/glatinone/mcpscan) first
Contributions welcome, open an issue or PR.
## License
MIT — see [LICENSE](LICENSE).
TDQS
A4/5.0
Scored across 9 tools
Disambiguation5/5
Each tool has a clear, distinct purpose: IOC handling (defang/refang/extract), hashing, password entropy, CIDR operations, shell command assessment, and repo scanning. No overlap or ambiguity.
Naming Consistency4/5
All tools use snake_case, but naming pattern varies between verb_noun (extract_iocs, scan_repo_root) and noun_noun (password_entropy, cidr_info). Mostly consistent but minor mixing.
Tool Count5/5
9 tools cover a broad range of secops tasks without being excessive. Each tool adds value, and the count is well-scoped for the server's purpose.
Completeness4/5
Covers core secops areas: IOC handling, hashing, password analysis, CIDR, shell assessment, repo scanning. Minor gaps like URL analysis missing, but no critical dead ends.
Maintenance
ActivitySlowing
ResponsivenessNo issues