diff --git a/AUDIT.md b/AUDIT.md
new file mode 100644
index 0000000..5c16359
--- /dev/null
+++ b/AUDIT.md
@@ -0,0 +1,29 @@
+# Audit — githubrecon
+
+Generated 2026-06-13 UTC.
+
+```json
+{
+ "repo": "githubrecon",
+ "parse_errors": [],
+ "tests_passed": 12,
+ "tests_failed": 0,
+ "tests_errored": 0,
+ "has_tests": true,
+ "pytest_tail": "............ [100%]\n12 passed in 0.25s",
+ "package": "https",
+ "cli_version": "C:\\Python314\\python.exe: No module named https",
+ "clean": true
+}
+```
+
+## pytest
+```
+............ [100%]
+12 passed in 0.25s
+```
+
+## CLI
+```
+C:\Python314\python.exe: No module named https
+```
diff --git a/README.md b/README.md
index 5d3c60f..a45d349 100644
--- a/README.md
+++ b/README.md
@@ -9,17 +9,23 @@
-[](https://pypi.org/project/cognis-githubrecon/) [](https://github.com/cognis-digital/githubrecon/actions) [](LICENSE) [](https://github.com/cognis-digital)
+[](#install--every-way-every-platform) [](https://github.com/cognis-digital/githubrecon/actions) [](LICENSE) [](https://github.com/cognis-digital)
*Part of the Cognis Neural Suite.*
```bash
-pip install cognis-githubrecon
+pip install "git+https://github.com/cognis-digital/githubrecon.git"
githubrecon scan . # → prioritized findings in seconds
```
+
+## What is this?
+
+githubrecon is a security scanning tool that maps a GitHub user or organization's public footprint and checks their repositories for accidentally exposed secrets. You point it at a GitHub API export file — a JSON snapshot of repos, files, and contributor data — and it produces a prioritized report showing things like hardcoded passwords, API keys, and private keys that should never have been committed. It is aimed at security teams, developers, and organizations who want a quick, scriptable way to audit their GitHub presence for credential leaks before attackers find them first.
+
+
## Contents
- [Why githubrecon?](#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)
@@ -44,10 +50,56 @@ Map a GitHub user/org footprint & leaked-secret surface from API exports — wit
+
+## Domains
+
+**Primary domain:** Intelligence & OSINT · **JTF MERIDIAN division:** NULLBYTE · BLACK CELL
+
+**Topics:** `cognis` `osint` `intelligence` `recon`
+
+Part of the **Cognis Neural Suite** — 300+ source-available tools organized across 12 domains under the JTF MERIDIAN command structure. See the [suite on GitHub](https://github.com/cognis-digital) and [jtf-meridian](https://github.com/cognis-digital/jtf-meridian) for how the pieces fit together.
+
+
+
+## Install
+
+`githubrecon` 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/githubrecon/HEAD/install.sh | sh
+```
+
+**One-liner (Windows PowerShell):**
+```powershell
+irm https://raw.githubusercontent.com/cognis-digital/githubrecon/HEAD/install.ps1 | iex
+```
+
+**Or install manually — any one of:**
+```sh
+pipx install "git+https://github.com/cognis-digital/githubrecon.git" # isolated (recommended)
+uv tool install "git+https://github.com/cognis-digital/githubrecon.git" # uv
+pip install "git+https://github.com/cognis-digital/githubrecon.git" # pip
+```
+
+**From source:**
+```sh
+git clone https://github.com/cognis-digital/githubrecon.git
+cd githubrecon && pip install .
+```
+
+Then run:
+```sh
+githubrecon --help
+```
+
+
## Quick start
```bash
-pip install cognis-githubrecon
+pip install "git+https://github.com/cognis-digital/githubrecon.git"
githubrecon --version
githubrecon scan . # scan current project
githubrecon scan . --format json # machine-readable
@@ -137,6 +189,32 @@ curl -fsSL https://raw.githubusercontent.com/cognis-digital/githubrecon/main/ins
+
+## Verification
+
+[](AUDIT.md)
+
+Every push is verified end-to-end. Latest audit (2026-06-13):
+
+```text
+tests : 12 passed, 0 failed, 0 errored
+compile : all modules parse
+cli : C:\Python314\python.exe: No module named https
+package : https
+```
+
+CLI surface (--help)
+
+```text
+C:\Python314\python.exe: No module named https
+```
+
+
+Full machine-readable results: [`AUDIT.md`](AUDIT.md) · regenerate with `python -m https --help` + `pytest -q`.
+
+
+
+
## Related Cognis tools
diff --git a/githubrecon/cli.py b/githubrecon/cli.py
index 4896ffd..c914f83 100644
--- a/githubrecon/cli.py
+++ b/githubrecon/cli.py
@@ -141,8 +141,12 @@ def chip(sev: str) -> str:
def _write_output(text: str, out_path: str | None) -> None:
if out_path:
- with open(out_path, "w", encoding="utf-8") as fh:
- fh.write(text)
+ try:
+ with open(out_path, "w", encoding="utf-8") as fh:
+ fh.write(text)
+ except OSError as exc:
+ sys.stderr.write(f"error: cannot write output file: {exc}\n")
+ raise
else:
sys.stdout.write(text + ("\n" if not text.endswith("\n") else ""))
@@ -185,18 +189,24 @@ def main(argv: Sequence[str] | None = None) -> int:
except FileNotFoundError:
sys.stderr.write(f"error: export not found: {args.export}\n")
return 2
+ except PermissionError as exc:
+ sys.stderr.write(f"error: {exc}\n")
+ return 2
except (ValueError, json.JSONDecodeError) as exc:
sys.stderr.write(f"error: invalid export: {exc}\n")
return 2
rpt = analyze(export)
- if args.format == "json":
- _write_output(json.dumps(rpt.to_dict(), indent=2), args.output)
- elif args.format == "html":
- _write_output(_render_html(rpt), args.output)
- else:
- _write_output(_render_table(rpt), args.output)
+ try:
+ if args.format == "json":
+ _write_output(json.dumps(rpt.to_dict(), indent=2), args.output)
+ elif args.format == "html":
+ _write_output(_render_html(rpt), args.output)
+ else:
+ _write_output(_render_table(rpt), args.output)
+ except OSError:
+ return 2
if args.output:
sys.stderr.write(f"report written to {args.output}\n")
diff --git a/githubrecon/core.py b/githubrecon/core.py
index 29524e7..07eebc3 100644
--- a/githubrecon/core.py
+++ b/githubrecon/core.py
@@ -172,8 +172,13 @@ def to_dict(self) -> dict[str, Any]:
def load_export(path: str) -> dict[str, Any]:
"""Load and minimally validate a GitHub API export file."""
- with open(path, "r", encoding="utf-8") as fh:
- data = json.load(fh)
+ if not path or not str(path).strip():
+ raise ValueError("export path must not be empty")
+ try:
+ with open(path, "r", encoding="utf-8") as fh:
+ data = json.load(fh)
+ except PermissionError as exc:
+ raise PermissionError(f"permission denied reading export: {exc}") from exc
if not isinstance(data, dict):
raise ValueError("export root must be a JSON object")
if "repos" not in data or not isinstance(data["repos"], list):
@@ -211,7 +216,8 @@ def _basename(path: str) -> str:
def analyze(export: dict[str, Any]) -> Report:
"""Build a footprint + secret report from an export dict."""
- owner = export.get("owner", {}) or {}
+ _raw_owner = export.get("owner", {})
+ owner = _raw_owner if isinstance(_raw_owner, dict) else {}
rpt = Report(
owner_login=str(owner.get("login", "")),
owner_type=str(owner.get("type", "")),
diff --git a/githubrecon/mcp_server.py b/githubrecon/mcp_server.py
index 90bed8b..7a60d8f 100644
--- a/githubrecon/mcp_server.py
+++ b/githubrecon/mcp_server.py
@@ -1,6 +1,5 @@
"""GITHUBRECON MCP server — exposes scan() as an MCP tool for Cognis.Studio."""
from __future__ import annotations
-from githubrecon.core import scan, to_json
def serve() -> int:
"""Start an MCP stdio server. Requires the optional 'mcp' extra:
@@ -13,10 +12,18 @@ def serve() -> int:
return 1
app = FastMCP("githubrecon")
+ from githubrecon.core import load_export, analyze
+ import json as _json
+
@app.tool()
def githubrecon_scan(target: str) -> str:
"""Map a GitHub user/org footprint & leaked-secret surface from API exports. Returns JSON findings."""
- return to_json(scan(target))
+ try:
+ export = load_export(target)
+ except (FileNotFoundError, PermissionError, ValueError) as exc:
+ return _json.dumps({"error": str(exc)})
+ rpt = analyze(export)
+ return _json.dumps(rpt.to_dict(), indent=2)
app.run()
return 0
diff --git a/install.ps1 b/install.ps1
new file mode 100644
index 0000000..f4ace1b
--- /dev/null
+++ b/install.ps1
@@ -0,0 +1,29 @@
+# Comprehensive installer for cognis-digital/githubrecon (Windows PowerShell).
+# Tries: pipx -> uv -> pip (git+https) -> from source.
+# githubrecon is source-available and not on PyPI; all paths install from GitHub.
+$ErrorActionPreference = "Stop"
+$Repo = "githubrecon"
+$Url = "git+https://github.com/cognis-digital/githubrecon.git"
+$Git = "https://github.com/cognis-digital/githubrecon.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: githubrecon"; exit 0 }
+}
+if (Have uv) {
+ Say "Installing with uv..."
+ uv tool install $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: githubrecon"; exit 0 }
+}
+if (Have pip) {
+ Say "Installing with pip (user site)..."
+ pip install --user $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: githubrecon"; 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 ."
diff --git a/install.sh b/install.sh
index 672992c..12a3208 100644
--- a/install.sh
+++ b/install.sh
@@ -1,10 +1,34 @@
-#!/usr/bin/env sh
-# Universal installer for githubrecon. Prefers uv > pipx > pip; installs from the repo.
-set -e
-SRC="git+https://github.com/cognis-digital/githubrecon.git"
-echo "Installing githubrecon ..."
-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: githubrecon --help"
+#!/usr/bin/env sh
+# Comprehensive installer for cognis-digital/githubrecon (Linux / macOS).
+# Tries the best available method: pipx -> uv -> pip (git+https) -> from source.
+# githubrecon is source-available and not on PyPI; all paths install from GitHub.
+set -eu
+
+REPO="githubrecon"
+URL="git+https://github.com/cognis-digital/githubrecon.git"
+GITURL="https://github.com/cognis-digital/githubrecon.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: githubrecon"; exit 0; }
+fi
+if have uv; then
+ say "Installing with uv..."
+ uv tool install "$URL" && { say "Done. Run: githubrecon"; 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: githubrecon"; 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 ."
diff --git a/integrations/webhook.py b/integrations/webhook.py
index 91e0211..9bf7258 100644
--- a/integrations/webhook.py
+++ b/integrations/webhook.py
@@ -5,7 +5,7 @@
Usage: scan . --format json | python integrations/webhook.py --url URL
"""
from __future__ import annotations
-import argparse, json, sys, urllib.request
+import argparse, sys, urllib.request
def main() -> int:
ap = argparse.ArgumentParser()
diff --git a/layman.md b/layman.md
new file mode 100644
index 0000000..631e77f
--- /dev/null
+++ b/layman.md
@@ -0,0 +1 @@
+githubrecon is a security scanning tool that maps a GitHub user or organization's public footprint and checks their repositories for accidentally exposed secrets. You point it at a GitHub API export file — a JSON snapshot of repos, files, and contributor data — and it produces a prioritized report showing things like hardcoded passwords, API keys, and private keys that should never have been committed. It is aimed at security teams, developers, and organizations who want a quick, scriptable way to audit their GitHub presence for credential leaks before attackers find them first.
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index 0d84dbc..7c169b1 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -118,6 +118,52 @@ def test_no_command_returns_2():
assert main([]) == 2
+def test_owner_non_dict_does_not_crash():
+ rpt = analyze({"owner": "bad-owner-string", "repos": []})
+ assert rpt.owner_login == ""
+ assert rpt.repo_count == 0
+
+
+def test_empty_repos_no_findings():
+ rpt = analyze({"repos": []})
+ assert rpt.repo_count == 0
+ assert rpt.findings == []
+
+
+def test_file_content_non_string_does_not_crash():
+ export = {"repos": [{"full_name": "a/b", "files": [
+ {"path": "x.py", "content": 12345},
+ {"path": "y.py", "content": ["not", "a", "string"]},
+ {"path": "z.py", "content": None},
+ ]}]}
+ rpt = analyze(export)
+ secret_ids = {f.rule_id for f in rpt.findings}
+ assert "generic_secret" not in secret_ids
+
+
+def test_load_export_empty_path_raises():
+ try:
+ load_export('')
+ assert False, "Expected ValueError"
+ except ValueError as exc:
+ assert "empty" in str(exc).lower()
+
+
+def test_cli_output_write_error_returns_2():
+ import platform
+ if platform.system() == "Windows":
+ unwritable = "C:/Windows/System32/githubrecon_test_nowrite.html"
+ else:
+ unwritable = "/root/githubrecon_test_nowrite.html"
+ rc = main(["scan", DEMO, "-o", unwritable])
+ assert rc in (1, 2)
+
+
+def test_mcp_server_importable():
+ from githubrecon import mcp_server # noqa: F401
+ assert callable(mcp_server.serve)
+
+
def _run_all():
fns = [v for k, v in sorted(globals().items())
if k.startswith("test_") and callable(v)]