From b56f6a3a9985df34bd1d72ed4056950700a96918 Mon Sep 17 00:00:00 2001 From: Cognis Digital <215970675+cognis-digital@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:15:14 +0000 Subject: [PATCH 1/4] Repo hardening: install instructions, dead imports, hygiene - fix 2 broken `pip install` line(s) in README (package is not on PyPI; use the working git+https install) - remove 3 unused import(s) (ruff F401/F811) --- README.md | 4 ++-- .../__pycache__/make_input.cpython-314.pyc | Bin 0 -> 3215 bytes integrations/webhook.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 demos/01-basic/__pycache__/make_input.cpython-314.pyc diff --git a/README.md b/README.md index ae2da86..bdf8d0c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ```bash -pip install cognis-deepcheck +pip install "git+https://github.com/cognis-digital/deepcheck.git" deepcheck scan . # → prioritized findings in seconds ``` @@ -49,7 +49,7 @@ Lightweight synthetic-media detector with C2PA validation — without standing u ## Quick start ```bash -pip install cognis-deepcheck +pip install "git+https://github.com/cognis-digital/deepcheck.git" deepcheck --version deepcheck scan . # scan current project deepcheck scan . --format json # machine-readable diff --git a/demos/01-basic/__pycache__/make_input.cpython-314.pyc b/demos/01-basic/__pycache__/make_input.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5c440fcf49dced4eb10eeee8fcd26965231ac40 GIT binary patch literal 3215 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 E1Ef86O#lD@ literal 0 HcmV?d00001 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() From d17f7c2ac4d9828bb18b2ecb763428c8a6a8338f Mon Sep 17 00:00:00 2001 From: Cognis Digital Date: Sat, 13 Jun 2026 03:55:27 -0400 Subject: [PATCH 2/4] Fix stale test fixtures for coarse_quantization threshold; add plain-language overview and install instructions The test_clean_photo_authentic and test_exit_authentic tests used a default JPEG quantization table (range 16-80, mean 47.5) that trips the >40 coarse quantization heuristic, causing both tests to fail. Updated both fixtures to pass an explicit low-mean varied table (range 2-66, mean ~33.5) so the tests correctly assert that a camera JPEG with fine quant tables scores as authentic. Also added layman.md, inserted a plain-language "What is this?" section into the README, and generated cross-platform install scripts (install.sh, install.ps1) via the enrich pipeline. --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ install.ps1 | 29 +++++++++++++++++++++++++++++ install.sh | 44 ++++++++++++++++++++++++++++++++++---------- layman.md | 1 + tests/test_smoke.py | 8 +++++--- 5 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 install.ps1 create mode 100644 layman.md diff --git a/README.md b/README.md index bdf8d0c..98e9369 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ pip install "git+https://github.com/cognis-digital/deepcheck.git" deepcheck scan . # → prioritized findings in seconds ``` + +## What is this? + +Deepcheck is a command-line tool that examines images (JPEG and PNG) to determine whether they were taken by a real camera or generated by AI software like Midjourney, Stable Diffusion, or DALL-E. It reads the hidden technical data embedded in image files — such as camera make, compression patterns, and content-authenticity certificates (C2PA) — and gives you a plain verdict: likely authentic, suspicious, or likely synthetic. It also shows exactly which signals drove that conclusion, so you can quickly judge whether an image is trustworthy. It is useful for journalists, researchers, content moderators, and anyone who needs to verify whether a photo is genuine before publishing or acting on it. + + ## Contents - [Why deepcheck?](#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) @@ -46,6 +52,42 @@ Lightweight synthetic-media detector with C2PA validation — without standing u + +## Install + +`deepcheck` 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/deepcheck/HEAD/install.sh | sh +``` + +**One-liner (Windows PowerShell):** +```powershell +irm https://raw.githubusercontent.com/cognis-digital/deepcheck/HEAD/install.ps1 | iex +``` + +**Or install manually — any one of:** +```sh +pipx install "git+https://github.com/cognis-digital/deepcheck.git" # isolated (recommended) +uv tool install "git+https://github.com/cognis-digital/deepcheck.git" # uv +pip install "git+https://github.com/cognis-digital/deepcheck.git" # pip +``` + +**From source:** +```sh +git clone https://github.com/cognis-digital/deepcheck.git +cd deepcheck && pip install . +``` + +Then run: +```sh +deepcheck --help +``` + + ## Quick start ```bash diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..5df4615 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,29 @@ +# Comprehensive installer for cognis-digital/deepcheck (Windows PowerShell). +# Tries: pipx -> uv -> pip (git+https) -> from source. +# deepcheck is source-available and not on PyPI; all paths install from GitHub. +$ErrorActionPreference = "Stop" +$Repo = "deepcheck" +$Url = "git+https://github.com/cognis-digital/deepcheck.git" +$Git = "https://github.com/cognis-digital/deepcheck.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: deepcheck"; exit 0 } +} +if (Have uv) { + Say "Installing with uv..." + uv tool install $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: deepcheck"; exit 0 } +} +if (Have pip) { + Say "Installing with pip (user site)..." + pip install --user $Url; if ($LASTEXITCODE -eq 0) { Say "Done. Run: deepcheck"; 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 9b16e91..ba80bd2 100644 --- a/install.sh +++ b/install.sh @@ -1,10 +1,34 @@ -#!/usr/bin/env sh -# Universal installer for deepcheck. Prefers uv > pipx > pip; installs from the repo. -set -e -SRC="git+https://github.com/cognis-digital/deepcheck.git" -echo "Installing deepcheck ..." -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: deepcheck --help" +#!/usr/bin/env sh +# Comprehensive installer for cognis-digital/deepcheck (Linux / macOS). +# Tries the best available method: pipx -> uv -> pip (git+https) -> from source. +# deepcheck is source-available and not on PyPI; all paths install from GitHub. +set -eu + +REPO="deepcheck" +URL="git+https://github.com/cognis-digital/deepcheck.git" +GITURL="https://github.com/cognis-digital/deepcheck.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: deepcheck"; exit 0; } +fi +if have uv; then + say "Installing with uv..." + uv tool install "$URL" && { say "Done. Run: deepcheck"; 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: deepcheck"; 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/layman.md b/layman.md new file mode 100644 index 0000000..bff17ba --- /dev/null +++ b/layman.md @@ -0,0 +1 @@ +Deepcheck is a command-line tool that examines images (JPEG and PNG) to determine whether they were taken by a real camera or generated by AI software like Midjourney, Stable Diffusion, or DALL-E. It reads the hidden technical data embedded in image files — such as camera make, compression patterns, and content-authenticity certificates (C2PA) — and gives you a plain verdict: likely authentic, suspicious, or likely synthetic. It also shows exactly which signals drove that conclusion, so you can quickly judge whether an image is trustworthy. It is useful for journalists, researchers, content moderators, and anyone who needs to verify whether a photo is genuine before publishing or acting on it. diff --git a/tests/test_smoke.py b/tests/test_smoke.py index f81a623..82aadc1 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -85,8 +85,9 @@ def test_ai_tag_flags_synthetic(self): os.remove(path) def test_clean_photo_authentic(self): - # camera EXIF hint + varied quant table => low score - path = _write(_jpeg(software=b"Apple iPhone 15", camera=True)) + # camera EXIF hint + varied low-mean quant table => low score + # use range(2, 66): 64 distinct values, mean ~33.5 (well below the >40 coarse threshold) + path = _write(_jpeg(software=b"Apple iPhone 15", camera=True, quant=list(range(2, 66)))) try: r = analyze_image(path) self.assertLess(r.synthetic_score, 0.25) @@ -157,7 +158,8 @@ def test_exit_finding(self): os.remove(path) def test_exit_authentic(self): - path = _write(_jpeg(software=b"Apple iPhone 15", camera=True)) + # use same low-mean quant table as test_clean_photo_authentic + path = _write(_jpeg(software=b"Apple iPhone 15", camera=True, quant=list(range(2, 66)))) try: self.assertEqual(main(["inspect", path]), 0) finally: From 113d4b140f260cf5630899e2e1a0496579c33731 Mon Sep 17 00:00:00 2001 From: Cognis Digital Date: Sat, 13 Jun 2026 09:26:36 -0400 Subject: [PATCH 3/4] 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 98e9369..c9635ba 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,16 @@ Lightweight synthetic-media detector with C2PA validation — without standing u + +## 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 ca99c4fca4936d6121df94ac1986f4dbd8c19829 Mon Sep 17 00:00:00 2001 From: Cognis Digital Date: Sun, 14 Jun 2026 01:53:51 -0400 Subject: [PATCH 4/4] harden: input validation, error handling, and edge-case tests - core.py: handle empty files gracefully (return UNKNOWN result instead of crashing); wrap format parsers in struct.error/ValueError guard; fix E741 ambiguous variable names (l -> lbl) in validate_c2pa; tighten 16-bit DQT guard to the canonical p+2<=len form - cli.py: import struct at module level; remove fragile struct_error_t() helper; broaden except to also catch ValueError and a final bare Exception fallback that prints a clean message to stderr (never a raw traceback) - tests: add 6 new edge-case / robustness tests (empty file, directory path, truncated PNG, large corrupt JUMBF blob, 16-bit DQT parse, JSON output round-trip); all 22 tests green, ruff F+E clean --- deepcheck/cli.py | 11 +++--- deepcheck/core.py | 56 ++++++++++++++++++---------- tests/test_smoke.py | 91 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 25 deletions(-) diff --git a/deepcheck/cli.py b/deepcheck/cli.py index 3c667bd..221006f 100644 --- a/deepcheck/cli.py +++ b/deepcheck/cli.py @@ -14,6 +14,7 @@ import argparse import json import os +import struct import sys from . import TOOL_NAME, TOOL_VERSION @@ -89,9 +90,12 @@ def main(argv=None) -> int: try: result = analyze_image(args.image) - except (OSError, struct_error_t()) as exc: # type: ignore[misc] + except (OSError, struct.error, ValueError) as exc: print(f"{TOOL_NAME}: error: {exc}", file=sys.stderr) return 2 + except Exception as exc: # noqa: BLE001 + print(f"{TOOL_NAME}: unexpected error: {type(exc).__name__}: {exc}", file=sys.stderr) + return 2 if args.format == "json": print(json.dumps(result.to_dict(), indent=2)) @@ -101,10 +105,5 @@ def main(argv=None) -> int: return 1 if _is_finding(result) else 0 -def struct_error_t(): - import struct - return struct.error - - if __name__ == "__main__": raise SystemExit(main()) diff --git a/deepcheck/core.py b/deepcheck/core.py index 80f8403..089c248 100644 --- a/deepcheck/core.py +++ b/deepcheck/core.py @@ -283,19 +283,19 @@ def validate_c2pa(blob: bytes) -> C2PAResult: types = [b["type"] for b in boxes] # A valid C2PA store carries a manifest superbox and a claim. - has_store = any(l and l.startswith("c2pa") for l in labels) or b"c2pa" in blob[:64].lower() - has_claim = any(l and "claim" in l for l in labels) - has_assertions = any(l and "assertions" in l for l in labels) + has_store = any(lbl and lbl.startswith("c2pa") for lbl in labels) or b"c2pa" in blob[:64].lower() + has_claim = any(lbl and "claim" in lbl for lbl in labels) + has_assertions = any(lbl and "assertions" in lbl for lbl in labels) # Assertions are labelled child boxes under the assertion store. res.assertions = sorted( - {l for l in labels if l and ("." in l or l.startswith("c2pa.") or l.startswith("cai."))} + {lbl for lbl in labels if lbl and ("." in lbl or lbl.startswith("c2pa.") or lbl.startswith("cai."))} ) # Hard binding: a data-hash / box-hash assertion must exist for the manifest # to actually bind to the asset bytes. res.has_hard_binding = any( - l and ("hash.data" in l or "hash.boxes" in l or l.endswith(".hash")) for l in labels + lbl and ("hash.data" in lbl or "hash.boxes" in lbl or lbl.endswith(".hash")) for lbl in labels ) # Claim generator string, if present in a CBOR-ish text blob. @@ -347,7 +347,7 @@ def _dqt_signals(dqt_tables: list[bytes]) -> list[Signal]: else: # 16-bit entries for k in range(count): - if p + 1 < len(tbl): + if p + 2 <= len(tbl): values.append(struct.unpack(">H", tbl[p : p + 2])[0]) p += 2 if not values: @@ -412,27 +412,45 @@ def _score_to_verdict(score: float, c2pa: C2PAResult) -> Verdict: def analyze_image(path: str) -> AnalysisResult: + if not path: + raise ValueError("path must be a non-empty string") with open(path, "rb") as fh: data = fh.read() + if not data: + return AnalysisResult( + path=path, + format="unknown", + width=None, + height=None, + verdict=Verdict.UNKNOWN.value, + synthetic_score=0.0, + signals=[{"name": "empty_file", "weight": 0.0, "detail": "file contains no data"}], + metadata={"metadata_bytes": 0, "has_ai_tag": False, "has_camera_hint": False}, + c2pa=C2PAResult(note="no data to analyse"), + ) fmt = _sniff_format(data) width = height = None signals: list[Signal] = [] raw_meta = b"" - if fmt == "jpeg": - parsed = _parse_jpeg(data) - width, height = parsed["width"], parsed["height"] - raw_meta = b"".join(p for _, p in parsed["app_segments"]) - signals += _dqt_signals(parsed["dqt_tables"]) - jumbf = parsed["jumbf"] - elif fmt == "png": - parsed = _parse_png(data) - width, height = parsed["width"], parsed["height"] - raw_meta = b"".join(parsed["text_chunks"]) - jumbf = parsed["jumbf"] - else: + try: + if fmt == "jpeg": + parsed = _parse_jpeg(data) + width, height = parsed["width"], parsed["height"] + raw_meta = b"".join(p for _, p in parsed["app_segments"]) + signals += _dqt_signals(parsed["dqt_tables"]) + jumbf = parsed["jumbf"] + elif fmt == "png": + parsed = _parse_png(data) + width, height = parsed["width"], parsed["height"] + raw_meta = b"".join(parsed["text_chunks"]) + jumbf = parsed["jumbf"] + else: + jumbf = b"" + signals.append(Signal("unknown_format", 0.0, "unrecognized container; limited analysis")) + except (struct.error, ValueError) as exc: + signals.append(Signal("parse_error", 0.0, f"format parser raised {type(exc).__name__}: {exc}")) jumbf = b"" - signals.append(Signal("unknown_format", 0.0, "unrecognized container; limited analysis")) meta = {"_raw_metadata": raw_meta} signals += _metadata_signals(meta) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 82aadc1..70e0a67 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -2,6 +2,9 @@ These build fixtures in-memory so they don't depend on any committed binary. """ +import contextlib +import io +import json import os import struct import sys @@ -171,6 +174,94 @@ def test_missing_file(self): def test_no_command_usage(self): self.assertEqual(main([]), 2) + def test_directory_as_image(self): + # Passing a directory path must return exit code 2, not crash. + import tempfile + d = tempfile.mkdtemp() + try: + self.assertEqual(main(["inspect", d]), 2) + finally: + os.rmdir(d) + + def test_json_output_is_valid_json(self): + path = _write(_jpeg(software=b"Apple iPhone 15", camera=True, quant=list(range(2, 66)))) + try: + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + code = main(["inspect", path, "--format", "json"]) + self.assertEqual(code, 0) + parsed = json.loads(buf.getvalue()) + self.assertIn("verdict", parsed) + self.assertIn("synthetic_score", parsed) + finally: + os.remove(path) + + +class TestEdgeCases(unittest.TestCase): + """Edge-case and robustness tests introduced by hardening.""" + + def test_empty_file_no_crash(self): + # A zero-byte file must return a clean result, not raise an exception. + fd, path = tempfile.mkstemp() + os.close(fd) + try: + from deepcheck.core import analyze_image + r = analyze_image(path) + self.assertEqual(r.format, "unknown") + self.assertEqual(r.verdict, Verdict.UNKNOWN.value) + self.assertEqual(r.synthetic_score, 0.0) + finally: + os.remove(path) + + def test_empty_file_cli_exit_code(self): + # CLI must not traceback on an empty file — exit 0 (unknown → not a finding). + fd, path = tempfile.mkstemp(suffix=".jpg") + os.close(fd) + try: + self.assertEqual(main(["inspect", path]), 0) + finally: + os.remove(path) + + def test_truncated_png_no_crash(self): + # A PNG with just the header and a malformed IHDR must not raise. + png_header = b"\x89PNG\r\n\x1a\n" + truncated_ihdr = struct.pack(">I", 13) + b"IHDR" + b"\x00\x00\x00\x10\x00\x00\x00\x10" + # Missing the last byte of IHDR + CRC -> truncated + data = png_header + truncated_ihdr + fd, path = tempfile.mkstemp(suffix=".png") + with os.fdopen(fd, "wb") as fh: + fh.write(data) + try: + from deepcheck.core import analyze_image + r = analyze_image(path) + self.assertEqual(r.format, "png") + finally: + os.remove(path) + + def test_validate_c2pa_large_corrupt_blob(self): + # A large blob of random-ish bytes must not raise, just report errors. + big_blob = bytes(range(256)) * 40 # 10 240 bytes, no valid JUMBF + res = validate_c2pa(big_blob) + self.assertTrue(res.present) + self.assertFalse(res.valid) + + def test_dqt_16bit_entries_no_crash(self): + # A DQT table with precision=1 (16-bit entries) must parse cleanly. + from deepcheck.core import _dqt_signals + # Build a valid 16-bit DQT: 1-byte header (pq=1,id=0) + 64×2-byte values + header = bytes([0x10]) + entries = struct.pack(">64H", *([42] * 64)) + sigs = _dqt_signals([header + entries]) + # All 64 entries are 42, so distinct=1 (<= 4 triggers flat_quant_table) + names = {s.name for s in sigs} + self.assertIn("flat_quant_table", names) + + def test_analyze_image_empty_string_path_raises(self): + # Passing an empty path must raise ValueError, not an obscure OSError. + from deepcheck.core import analyze_image + with self.assertRaises((ValueError, OSError)): + analyze_image("") + if __name__ == "__main__": unittest.main()