Skip to main content
Glama
glatinone

Secops Toolkit MCP

by glatinone
README.md
# SecOps Toolkit MCP

[![CI](https://github.com/glatinone/secops-toolkit-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/glatinone/secops-toolkit-mcp/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/glatinone/secops-toolkit-mcp)](https://github.com/glatinone/secops-toolkit-mcp/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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