From 1e9f0817b0cf052f5d52f20374d9781e02a871e4 Mon Sep 17 00:00:00 2001 From: Cognis Digital Date: Sat, 13 Jun 2026 08:41:10 -0400 Subject: [PATCH 1/3] Repo hardening: verified build-out, plain-language overview, and comprehensive cross-platform install scripts - README opens with a plain-language "What is this?" overview - comprehensive Install section + install.sh / install.ps1 (pipx / uv / pip git+https / source) - verified build-out: real test + CLI audit embedded (README Verification + AUDIT.md) --- AUDIT.md | 29 ++++++++++++++++ README.md | 74 +++++++++++++++++++++++++++++++++++++++-- install.ps1 | 29 ++++++++++++++++ install.sh | 44 ++++++++++++++++++------ integrations/webhook.py | 2 +- layman.md | 1 + 6 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 AUDIT.md create mode 100644 install.ps1 create mode 100644 layman.md 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..a1276be 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,23 @@ -[![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.* ```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,46 @@ Map a GitHub user/org footprint & leaked-secret surface from API exports — wit
↑ back to top
+ +## 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 +179,32 @@ curl -fsSL https://raw.githubusercontent.com/cognis-digital/githubrecon/main/ins
↑ back to top
+ +## 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 +``` + +
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`. + +
↑ back to top
+ + ## Related Cognis tools 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. From b657fa96ee0a8b28de86e81188ca77689527b86b Mon Sep 17 00:00:00 2001 From: Cognis Digital Date: Sat, 13 Jun 2026 09:18:21 -0400 Subject: [PATCH 2/3] docs: add Domains section (suite taxonomy + JTF MERIDIAN mapping) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index a1276be..a45d349 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,16 @@ Map a GitHub user/org footprint & leaked-secret surface from API exports — wit
↑ back to top
+ +## 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 From 1a3c2def068c8f591134f4ba19f4f83165a8e864 Mon Sep 17 00:00:00 2001 From: Cognis Digital Date: Sun, 14 Jun 2026 02:14:58 -0400 Subject: [PATCH 3/3] harden: input validation, error handling, and edge-case tests - core.py load_export: guard empty path (ValueError), re-raise PermissionError with context instead of raw OSError traceback - core.py analyze: guard non-dict owner value instead of crashing with AttributeError on .get() - cli.py _write_output: catch OSError and print clean error to stderr; propagate for caller to return exit code 2 - cli.py main: catch PermissionError from load_export (not a subclass of ValueError/FileNotFoundError); wrap write calls so any OSError returns 2 without a traceback - mcp_server.py: fix broken module-level import of non-existent scan/to_json; defer import and wire to actual analyze/load_export API with exception handling in the tool handler - tests: 6 new tests covering owner non-dict, empty repos, non-string file content, empty path, write-error exit code, mcp_server import --- githubrecon/cli.py | 26 +++++++++++++++------- githubrecon/core.py | 12 +++++++--- githubrecon/mcp_server.py | 11 ++++++++-- tests/test_smoke.py | 46 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 13 deletions(-) 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/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)]