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
29 changes: 29 additions & 0 deletions AUDIT.md
Original file line number Diff line number Diff line change
@@ -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
```
84 changes: 81 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@

<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&size=18&duration=3500&pause=1000&color=6B46C1&center=true&vCenter=true&width=720&lines=Map+a+GitHub+userorg+footprint++leakedsecret+surface+from+AP;Self-hostable+%C2%B7+MCP-native+%C2%B7+CI-ready+%C2%B7+polyglot" width="720"/>

[![PyPI](https://img.shields.io/pypi/v/cognis-githubrecon.svg?color=6b46c1)](https://pypi.org/project/cognis-githubrecon/) [![CI](https://github.com/cognis-digital/githubrecon/actions/workflows/ci.yml/badge.svg)](https://github.com/cognis-digital/githubrecon/actions) [![License: COCL 1.0](https://img.shields.io/badge/License-COCL%201.0-2b6cb0.svg)](LICENSE) [![Suite](https://img.shields.io/badge/Cognis-Neural%20Suite-6b46c1.svg)](https://github.com/cognis-digital)
[![install](https://img.shields.io/badge/install-git%2B%20%C2%B7%20pipx%20%C2%B7%20uv-6b46c1.svg)](#install--every-way-every-platform) [![CI](https://github.com/cognis-digital/githubrecon/actions/workflows/ci.yml/badge.svg)](https://github.com/cognis-digital/githubrecon/actions) [![License: COCL 1.0](https://img.shields.io/badge/License-COCL%201.0-2b6cb0.svg)](LICENSE) [![Suite](https://img.shields.io/badge/Cognis-Neural%20Suite-6b46c1.svg)](https://github.com/cognis-digital)

*Part of the Cognis Neural Suite.*

</div>

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

<!-- cognis:layman:start -->
## 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.
<!-- cognis:layman:end -->

## 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)
Expand All @@ -44,10 +50,56 @@ Map a GitHub user/org footprint & leaked-secret surface from API exports — wit
<div align="right"><a href="#top">↑ back to top</a></div>

<a name="quick-start"></a>
<!-- cognis:domains:start -->
## 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.
<!-- cognis:domains:end -->

<!-- cognis:install:start -->
## 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
```
<!-- cognis:install:end -->

## 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
Expand Down Expand Up @@ -137,6 +189,32 @@ curl -fsSL https://raw.githubusercontent.com/cognis-digital/githubrecon/main/ins
<div align="right"><a href="#top">↑ back to top</a></div>

<a name="related"></a>
<a name="verification"></a>
## Verification

[![tests](https://img.shields.io/badge/tests-12%20passing-2ea44f.svg)](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
```

<details><summary>CLI surface (<code>--help</code>)</summary>

```text
C:\Python314\python.exe: No module named https
```
</details>

Full machine-readable results: [`AUDIT.md`](AUDIT.md) · regenerate with `python -m https --help` + `pytest -q`.

<div align="right"><a href="#top">↑ back to top</a></div>


## Related Cognis tools


Expand Down
26 changes: 18 additions & 8 deletions githubrecon/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""))

Expand Down Expand Up @@ -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")
Expand Down
12 changes: 9 additions & 3 deletions githubrecon/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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", "")),
Expand Down
11 changes: 9 additions & 2 deletions githubrecon/mcp_server.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
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/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 ."
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 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 ."
2 changes: 1 addition & 1 deletion integrations/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Usage: <tool> 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()
Expand Down
1 change: 1 addition & 0 deletions layman.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading