Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
</div>

```bash
pip install cognis-keyhunt
pip install "git+https://github.com/cognis-digital/keyhunt.git"
keyhunt scan . # → prioritized findings in seconds
```

<!-- cognis:layman:start -->
## What is this?

keyhunt scans firmware images, router configuration files, and filesystem dumps for secrets that should never have been shipped — things like hardcoded passwords, private encryption keys, embedded API tokens, and default login credentials. Point it at a file or folder and it returns a prioritized list of findings in seconds, ready for your terminal, a CI pipeline, or an AI agent. It is aimed at security researchers, IoT/embedded developers, and product teams who need to catch credential leaks before a device ships or after one shows up in the wild.
<!-- cognis:layman:end -->

## Contents

- [Why keyhunt?](#why) · [Features](#features) · [Quick start](#quick-start) · [Example](#example) · [Architecture](#architecture) · [AI stack](#ai-stack) · [How it compares](#how-it-compares) · [Integrations](#integrations) · [Install anywhere](#install-anywhere) · [Related](#related) · [Contributing](#contributing)
Expand All @@ -46,10 +52,46 @@ Instant gratification — point at any router firmware and get 'hardcoded root S
<div align="right"><a href="#top">↑ back to top</a></div>

<a name="quick-start"></a>
<!-- cognis:install:start -->
## Install

`keyhunt` is source-available (not published to PyPI) — every method below installs
straight from GitHub. Pick whichever you prefer; the one-line scripts auto-detect
the best tool available on your machine.

**One-liner (Linux / macOS):**
```sh
curl -fsSL https://raw.githubusercontent.com/cognis-digital/keyhunt/HEAD/install.sh | sh
```

**One-liner (Windows PowerShell):**
```powershell
irm https://raw.githubusercontent.com/cognis-digital/keyhunt/HEAD/install.ps1 | iex
```

**Or install manually — any one of:**
```sh
pipx install "git+https://github.com/cognis-digital/keyhunt.git" # isolated (recommended)
uv tool install "git+https://github.com/cognis-digital/keyhunt.git" # uv
pip install "git+https://github.com/cognis-digital/keyhunt.git" # pip
```

**From source:**
```sh
git clone https://github.com/cognis-digital/keyhunt.git
cd keyhunt && pip install .
```

Then run:
```sh
keyhunt --help
```
<!-- cognis:install:end -->

## Quick start

```bash
pip install cognis-keyhunt
pip install "git+https://github.com/cognis-digital/keyhunt.git"
keyhunt --version
keyhunt scan . # scan current project
keyhunt scan . --format json # machine-readable
Expand Down
2 changes: 1 addition & 1 deletion demos/01-basic/firmware_dump.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ---- extracted from /etc/init.d/rcS (busybox) ----
# default management login left in the image
telnetd -l /bin/sh -b 0.0.0.0 admin
busybox telnetd -l admin -p 23 -b 0.0.0.0

# ---- /etc/config/httpd.conf ----
admin_user=admin
Expand Down
29 changes: 29 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Comprehensive installer for cognis-digital/keyhunt (Windows PowerShell).
# Tries: pipx -> uv -> pip (git+https) -> from source.
# keyhunt is source-available and not on PyPI; all paths install from GitHub.
$ErrorActionPreference = "Stop"
$Repo = "keyhunt"
$Url = "git+https://github.com/cognis-digital/keyhunt.git"
$Git = "https://github.com/cognis-digital/keyhunt.git"
function Say($m) { Write-Host "[$Repo] $m" -ForegroundColor Magenta }
function Have($c) { [bool](Get-Command $c -ErrorAction SilentlyContinue) }

if (-not (Have python) -and -not (Have py)) {
Say "Python 3.9+ is required but was not found. Install Python first."; exit 1
}
if (Have pipx) {
Say "Installing with pipx (isolated, recommended)..."
pipx install $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: keyhunt"; exit 0 }
}
if (Have uv) {
Say "Installing with uv..."
uv tool install $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: keyhunt"; exit 0 }
}
if (Have pip) {
Say "Installing with pip (user site)..."
pip install --user $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: keyhunt"; exit 0 }
}
Say "No packaging tool worked; falling back to a source clone."
$Tmp = Join-Path $env:TEMP "$Repo-src"
git clone --depth 1 $Git $Tmp
Say "Cloned to $Tmp - run: cd $Tmp; python -m pip install ."
44 changes: 34 additions & 10 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
#!/usr/bin/env sh
# Universal installer for keyhunt. Prefers uv > pipx > pip; installs from the repo.
set -e
SRC="git+https://github.com/cognis-digital/keyhunt.git"
echo "Installing keyhunt ..."
if command -v uv >/dev/null 2>&1; then uv tool install "$SRC"
elif command -v pipx >/dev/null 2>&1; then pipx install "$SRC"
elif command -v python3 >/dev/null 2>&1; then python3 -m pip install --user "$SRC"
else echo "Need uv, pipx, or python3+pip"; exit 1; fi
echo "Done. Run: keyhunt --help"
#!/usr/bin/env sh
# Comprehensive installer for cognis-digital/keyhunt (Linux / macOS).
# Tries the best available method: pipx -> uv -> pip (git+https) -> from source.
# keyhunt is source-available and not on PyPI; all paths install from GitHub.
set -eu

REPO="keyhunt"
URL="git+https://github.com/cognis-digital/keyhunt.git"
GITURL="https://github.com/cognis-digital/keyhunt.git"

say() { printf '\033[1;35m[%s]\033[0m %s\n' "$REPO" "$1"; }
have() { command -v "$1" >/dev/null 2>&1; }

if ! have python3 && ! have python; then
say "Python 3.9+ is required but was not found. Install Python first."; exit 1
fi

if have pipx; then
say "Installing with pipx (isolated, recommended)..."
pipx install "$URL" && { say "Done. Run: keyhunt"; exit 0; }
fi
if have uv; then
say "Installing with uv..."
uv tool install "$URL" && { say "Done. Run: keyhunt"; exit 0; }
fi
if have pip3 || have pip; then
PIP="$(command -v pip3 || command -v pip)"
say "Installing with pip (user site)..."
"$PIP" install --user "$URL" && { say "Done. Run: keyhunt"; exit 0; }
fi

say "No packaging tool worked; falling back to a source clone."
TMP="$(mktemp -d)"; git clone --depth 1 "$GITURL" "$TMP/$REPO"
say "Cloned to $TMP/$REPO — run: cd $TMP/$REPO && python3 -m pip install ."
4 changes: 3 additions & 1 deletion integrations/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
Usage: <tool> scan . --format json | python integrations/webhook.py --url URL
"""
from __future__ import annotations
import argparse, json, sys, urllib.request
import argparse
import sys
import urllib.request

def main() -> int:
ap = argparse.ArgumentParser()
Expand Down
20 changes: 12 additions & 8 deletions keyhunt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,18 @@ def _render_json(findings: List[Finding], show_secrets: bool) -> str:

def main(argv: Optional[List[str]] = None) -> int:
parser = _build_parser()
args = parser.parse_args(argv)
try:
args = parser.parse_args(argv)
except SystemExit as exc:
# argparse already printed usage/error; map to exit code 2
return int(exc.code) if exc.code is not None else 2

if not args.command:
parser.print_help()
parser.print_help(sys.stderr)
return 2

if args.command == "scan":
fmt = args.format or args.format if args.format else None
# subcommand --format overrides global; fall back to global default
fmt = args.format if getattr(args, "format", None) else "table"
# argparse stores global --format on the same attribute name; resolve:
# subcommand --format overrides global; fall back to "table"
fmt = args.format or "table"

if not os.path.exists(args.path):
Expand All @@ -134,9 +135,12 @@ def main(argv: Optional[List[str]] = None) -> int:

try:
findings = scan_path(args.path)
except Exception as exc: # pragma: no cover - defensive
except (OSError, FileNotFoundError) as exc:
print(f"{TOOL_NAME}: error: {exc}", file=sys.stderr)
return 2
except Exception as exc: # pragma: no cover - last-resort safety net
print(f"{TOOL_NAME}: unexpected error: {exc}", file=sys.stderr)
return 2

findings = _filter_severity(findings, args.severity)

Expand All @@ -147,7 +151,7 @@ def main(argv: Optional[List[str]] = None) -> int:

return 1 if findings else 0

parser.print_help()
parser.print_help(sys.stderr)
return 2


Expand Down
39 changes: 35 additions & 4 deletions keyhunt/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
"""
from __future__ import annotations

import json
import math
import os
import re
from dataclasses import dataclass, field, asdict
from typing import Iterable, Iterator, List, Optional
from dataclasses import dataclass, asdict
from typing import Iterator, List, Optional

TOOL_NAME = "keyhunt"
TOOL_VERSION = "0.1.0"

# ---------------------------------------------------------------------------
# Detector definitions
Expand Down Expand Up @@ -128,7 +132,7 @@ def _shannon_entropy(s: str) -> float:
description="Hardcoded password assignment",
severity="high",
regex=re.compile(
r"(?i)(?:^|[^A-Za-z0-9_])(?:passwd|password|pwd|admin_pass|root_pass)"
r"(?i)(?:^|[^A-Za-z0-9_])(?:passwd|password|pwd|admin_pass|admin_password|root_pass|root_password)"
r"\s*[:=]\s*['\"]([^'\"\n]{3,64})['\"]"
),
secret_group=1,
Expand Down Expand Up @@ -303,11 +307,38 @@ def scan_path(
skip_ext: Optional[set] = None,
max_bytes: int = DEFAULT_MAX_BYTES,
) -> List[Finding]:
"""Scan a file or directory tree and return all findings."""
"""Scan a file or directory tree and return all findings.

Raises FileNotFoundError if *root* does not exist.
Returns an empty list if the tree contains no scannable files.
"""
if not os.path.exists(root):
raise FileNotFoundError(f"path not found: {root}")
findings: List[Finding] = []
for path in iter_files(root, skip_ext=skip_ext, max_bytes=max_bytes):
findings.extend(scan_file(path, max_bytes=max_bytes))
# Stable, useful ordering: severity then path then line.
sev_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
findings.sort(key=lambda f: (sev_rank.get(f.severity, 9), f.path, f.line))
return findings


# ---------------------------------------------------------------------------
# Convenience aliases (used by mcp_server and external callers)
# ---------------------------------------------------------------------------


def scan(target: str, **kwargs) -> List[Finding]:
"""Alias for scan_path; raises FileNotFoundError on missing target."""
return scan_path(target, **kwargs)


def to_json(findings: List[Finding], *, redact: bool = True) -> str:
"""Serialise a list of Finding objects to a JSON string."""
payload = {
"tool": TOOL_NAME,
"version": TOOL_VERSION,
"count": len(findings),
"findings": [f.to_dict(redact=redact) for f in findings],
}
return json.dumps(payload, indent=2)
53 changes: 31 additions & 22 deletions keyhunt/mcp_server.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
"""KEYHUNT MCP server — exposes scan() as an MCP tool for Cognis.Studio."""
from __future__ import annotations
from keyhunt.core import scan, to_json

def serve() -> int:
"""Start an MCP stdio server. Requires the optional 'mcp' extra:
pip install "cognis-keyhunt[mcp]"
"""
try:
from mcp.server.fastmcp import FastMCP
except Exception:
print("Install the MCP extra: pip install 'cognis-keyhunt[mcp]'")
return 1
app = FastMCP("keyhunt")

@app.tool()
def keyhunt_scan(target: str) -> str:
"""Scan firmware blobs and filesystem dumps for hardcoded private keys, API tokens, default creds, and weak RSA/ECC material.. Returns JSON findings."""
return to_json(scan(target))

app.run()
return 0
"""KEYHUNT MCP server — exposes scan() as an MCP tool for Cognis.Studio."""
from __future__ import annotations

from keyhunt.core import scan, to_json


def serve() -> int:
"""Start an MCP stdio server. Requires the optional 'mcp' extra:
pip install "cognis-keyhunt[mcp]"
"""
try:
from mcp.server.fastmcp import FastMCP
except Exception:
print("Install the MCP extra: pip install 'cognis-keyhunt[mcp]'")
return 1
app = FastMCP("keyhunt")

@app.tool()
def keyhunt_scan(target: str) -> str:
"""Scan firmware blobs and filesystem dumps for hardcoded keys.

Detects private keys, API tokens, default creds, and weak RSA/ECC
material. Returns JSON findings.
"""
try:
return to_json(scan(target))
except FileNotFoundError:
return to_json([]) # return empty findings on bad path

app.run()
return 0
1 change: 1 addition & 0 deletions layman.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
keyhunt scans firmware images, router configuration files, and filesystem dumps for secrets that should never have been shipped — things like hardcoded passwords, private encryption keys, embedded API tokens, and default login credentials. Point it at a file or folder and it returns a prioritized list of findings in seconds, ready for your terminal, a CI pipeline, or an AI agent. It is aimed at security researchers, IoT/embedded developers, and product teams who need to catch credential leaks before a device ships or after one shows up in the wild.
Loading
Loading