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
1 change: 1 addition & 0 deletions ghost-ai-scanner/src/chat/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"get_shadow_ai_census": get_shadow_ai_census,
"get_recent_activity": get_recent_activity,
"compare_periods": compare_periods,

}

# Tools that take **kwargs only (no scope).
Expand Down
137 changes: 137 additions & 0 deletions scripts/check_code_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
# =============================================================================
# scripts/check_code_model.py — Structural model compliance checker
# Author: Giggso Inc / Ravi Venugopal
# Purpose: Verifies code follows PatronAI/Marauder Scan build patterns
# =============================================================================
# | Date | Author | Change |
# |------------|--------|--------------------------------|
# | 2026-05-08 | RV | Initial implementation |
# =============================================================================

import ast
import re
import sys
import subprocess
from pathlib import Path

CX_ORACLE_RE = re.compile(r"\bimport cx_Oracle\b|from cx_Oracle\b")
RAW_FITZ_RE = re.compile(r"^import fitz\b", re.MULTILINE)
DOTENV_IN_FN_RE = re.compile(r"def\s+\w+[^:]*:\s*\n(?:[^\n]*\n)*?[^\n]*load_dotenv\(\)")
CAMEL_VAR_RE = re.compile(r"^\s{4,}[a-z]+[A-Z]\w*\s*=", re.MULTILINE)
LLM_CALL_RE = re.compile(r"\.chat\.completions\.create|openai\.ChatCompletion")
PREPROCESS_RE = re.compile(r"preprocess_text\s*\(")
TEMP_ZERO_RE = re.compile(r"temperature\s*=\s*0\b")
COUNT_TOKENS_RE = re.compile(r"count_tokens\s*\(")


def get_staged_files() -> list[str]:
"""Return staged Python files via git diff."""
try:
r = subprocess.run(
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
capture_output=True, text=True, check=True,
)
return [f for f in r.stdout.strip().splitlines() if f.endswith(".py")]
except subprocess.CalledProcessError as e:
sys.stderr.write(f"[model] git diff failed: {e}\n")
return []


def check_import_order(tree: ast.Module, path: str) -> list[str]:
"""Flag imports that appear after function/class definitions."""
v: list[str] = []
saw_def = False
for node in ast.iter_child_nodes(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
saw_def = True
elif isinstance(node, (ast.Import, ast.ImportFrom)) and saw_def:
v.append(
f"[MODEL FAIL] {path}:{node.lineno} — import after definition — "
"move all imports to the top of the file"
)
return v


def check_llm_conventions(content: str, path: str) -> list[str]:
"""Verify LLM calls follow preprocess → count_tokens → temperature=0 pattern."""
v: list[str] = []
if not LLM_CALL_RE.search(content):
return v
if not PREPROCESS_RE.search(content):
v.append(
f"[MODEL FAIL] {path} — LLM call without preprocess_text() — "
"strip noise before sending to API"
)
if not COUNT_TOKENS_RE.search(content):
v.append(
f"[MODEL FAIL] {path} — LLM call without count_tokens() — "
"log token count to stderr before every API call"
)
if not TEMP_ZERO_RE.search(content):
v.append(
f"[MODEL FAIL] {path} — LLM call missing temperature=0 — "
"enforce deterministic output on all API calls"
)
return v


def check_file(path: str) -> list[str]:
"""Run all model compliance rules on one file."""
v: list[str] = []
try:
content = Path(path).read_text(encoding="utf-8", errors="ignore")
except OSError as e:
return [f"[MODEL FAIL] {path} — cannot read: {e}"]

if CX_ORACLE_RE.search(content):
v.append(
f"[MODEL FAIL] {path} — cx_Oracle import — "
"replace with oracledb (CLAUDE.md stack rule)"
)
if RAW_FITZ_RE.search(content):
v.append(
f"[MODEL FAIL] {path} — bare 'import fitz' — "
"use 'import pymupdf as fitz' instead"
)
if CAMEL_VAR_RE.search(content):
v.append(
f"[MODEL FAIL] {path} — camelCase variable detected — "
"use snake_case for all variables and functions"
)

v.extend(check_llm_conventions(content, path))

try:
tree = ast.parse(content)
v.extend(check_import_order(tree, path))
except SyntaxError:
pass # syntax errors are reported by check_code_quality.py

return v


def main() -> int:
"""Entry point. Exit 0 = pass, 1 = violations found."""
files = sys.argv[1:] or get_staged_files()
if not files:
print("[MODEL PASS] No Python files to check")
return 0

violations: list[str] = []
for path in files:
violations.extend(check_file(path))

for msg in violations:
print(msg)

if not violations:
print("[MODEL PASS] All structural model checks passed")
return 0

print(f"\n[MODEL BLOCK] {len(violations)} violation(s) — fix before push")
return 1


if __name__ == "__main__":
sys.exit(main())
135 changes: 135 additions & 0 deletions scripts/check_code_quality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# =============================================================================
# scripts/check_code_quality.py — CLAUDE.md code quality gate
# Author: Giggso Inc / Ravi Venugopal
# Purpose: AST-based checker for pre-push quality enforcement
# =============================================================================
# | Date | Author | Change |
# |------------|--------|--------------------------------|
# | 2026-05-08 | RV | Initial implementation |
# =============================================================================

import ast
import re
import sys
import subprocess
from pathlib import Path

MAX_LINES = 150
HEADER_RE = re.compile(r"#.*?(file:|purpose:|author:|={10,})", re.IGNORECASE)
AUDIT_RE = re.compile(r"#.*?audit\s*log", re.IGNORECASE)
CRED_RE = re.compile(
r'(api_key|password|secret_key|token|passwd)\s*=\s*["\'][^"\']{4,}',
re.IGNORECASE,
)
PRINT_RE = re.compile(r"^\s*print\s*\(", re.MULTILINE)
MCP_PATH_RE = re.compile(r"mcp", re.IGNORECASE)
EXTERNAL_CALLS = re.compile(
r"\b(requests\.|boto3\.|openai\.|subprocess\.|open\(|oracledb\.)", re.MULTILINE
)


def get_staged_files() -> list[str]:
"""Return staged Python files via git diff."""
try:
r = subprocess.run(
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
capture_output=True, text=True, check=True,
)
return [f for f in r.stdout.strip().splitlines() if f.endswith(".py")]
except subprocess.CalledProcessError as e:
sys.stderr.write(f"[quality] git diff failed: {e}\n")
return []


def _line_no(content: str, offset: int) -> int:
"""Convert string offset to 1-based line number."""
return content[:offset].count("\n") + 1


def check_file(path: str) -> list[str]:
"""Run all quality rules on one file. Returns list of violation strings."""
v: list[str] = []
try:
content = Path(path).read_text(encoding="utf-8", errors="ignore")
except OSError as e:
return [f"[QUALITY FAIL] {path} — cannot read: {e} — check file permissions"]

lines = content.splitlines()

if len(lines) > MAX_LINES:
v.append(
f"[QUALITY FAIL] {path}:{len(lines)} — exceeds 150-line limit "
f"({len(lines)} lines) — split into smaller modules"
)
if not HEADER_RE.search(content[:600]):
v.append(
f"[QUALITY FAIL] {path}:1 — missing file header — "
"add filename/purpose/author block at top"
)
if not AUDIT_RE.search(content):
v.append(
f"[QUALITY FAIL] {path}:1 — missing audit log table — "
"add '# | Date | Author | Change |' table"
)
for m in CRED_RE.finditer(content):
v.append(
f"[QUALITY FAIL] {path}:{_line_no(content, m.start())} — "
f"hardcoded credential '{m.group(1)}' — use os.getenv() + .env"
)
if MCP_PATH_RE.search(path):
for m in PRINT_RE.finditer(content):
v.append(
f"[QUALITY FAIL] {path}:{_line_no(content, m.start())} — "
"print() in MCP server — use sys.stderr.write() instead"
)

try:
tree = ast.parse(content)
except SyntaxError as e:
v.append(f"[QUALITY FAIL] {path}:{e.lineno} — syntax error: {e.msg}")
return v

for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
args = node.args.args + node.args.posonlyargs + node.args.kwonlyargs
missing = [a.arg for a in args if a.annotation is None and a.arg != "self"]
if missing or node.returns is None:
v.append(
f"[QUALITY FAIL] {path}:{node.lineno} — {node.name}() missing "
f"type hints ({', '.join(missing) or 'return'}) — annotate all params + return"
)
if not ast.get_docstring(node):
v.append(
f"[QUALITY FAIL] {path}:{node.lineno} — {node.name}() "
"missing docstring — add purpose comment"
)

return v


def main() -> int:
"""Entry point. Exit 0 = pass, 1 = violations found."""
files = sys.argv[1:] or get_staged_files()
if not files:
print("[QUALITY PASS] No Python files to check")
return 0

violations: list[str] = []
for path in files:
violations.extend(check_file(path))

for msg in violations:
print(msg)

if not violations:
print("[QUALITY PASS] All quality checks passed")
return 0

print(f"\n[QUALITY BLOCK] {len(violations)} violation(s) — fix before push")
return 1


if __name__ == "__main__":
sys.exit(main())
71 changes: 71 additions & 0 deletions scripts/hooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# =============================================================================
# scripts/hooks/pre-push — PatronAI pre-push quality gate
# Author: Giggso Inc / Ravi Venugopal
# Purpose: Blocks push on quality/model/vuln failures; advisory PR review async
# =============================================================================
# | Date | Author | Change |
# |------------|--------|--------------------------------|
# | 2026-05-08 | RV | Initial implementation |
# =============================================================================

set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON="${PYTHON:-python3}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN} PatronAI Pre-Push Quality Gate${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"

# ── Step 1: Code Quality ─────────────────────────────────────────────────────
echo ""
echo -e "▶ ${CYAN}[1/3] Code Quality Check...${NC}"
if ! "$PYTHON" "$REPO_ROOT/scripts/check_code_quality.py"; then
echo ""
echo -e "${RED}✗ PUSH BLOCKED — Code quality violations detected above.${NC}"
echo -e "${YELLOW} → Fix all [QUALITY FAIL] items, re-stage, and retry push.${NC}"
exit 1
fi
echo -e "${GREEN} ✓ Code quality passed${NC}"

# ── Step 2: Code Model ───────────────────────────────────────────────────────
echo ""
echo -e "▶ ${CYAN}[2/3] Structural Model Check...${NC}"
if ! "$PYTHON" "$REPO_ROOT/scripts/check_code_model.py"; then
echo ""
echo -e "${RED}✗ PUSH BLOCKED — Structural model violations detected above.${NC}"
echo -e "${YELLOW} → Fix all [MODEL FAIL] items, re-stage, and retry push.${NC}"
exit 1
fi
echo -e "${GREEN} ✓ Code model compliance passed${NC}"

# ── Step 3: Vulnerability Scan ───────────────────────────────────────────────
echo ""
echo -e "▶ ${CYAN}[3/3] GPT-5.5 Vulnerability Scan...${NC}"
if ! "$PYTHON" "$REPO_ROOT/scripts/vuln_scan.py"; then
echo ""
echo -e "${RED}✗ PUSH BLOCKED — CRITICAL/HIGH vulnerabilities found above.${NC}"
echo -e "${YELLOW} → Fix all [VULN CRITICAL] and [VULN HIGH] items, re-stage, and retry push.${NC}"
exit 1
fi
echo -e "${GREEN} ✓ Vulnerability scan passed${NC}"

# ── All gates passed ─────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✓ All gates passed — push proceeding${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════${NC}"
echo ""

# ── PR Review (async, advisory — never blocks push) ──────────────────────────
"$PYTHON" "$REPO_ROOT/scripts/pr_review.py" &
disown
echo -e "${CYAN} ↪ PR review running in background — check GitHub for comments${NC}"
echo ""
41 changes: 41 additions & 0 deletions scripts/install-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# =============================================================================
# scripts/install-hooks.sh — Install PatronAI git hooks from scripts/hooks/
# Author: Giggso Inc / Ravi Venugopal
# Purpose: One-time setup; symlinks scripts/hooks/* into .git/hooks/
# =============================================================================
# | Date | Author | Change |
# |------------|--------|--------------------------------|
# | 2026-05-08 | RV | Initial implementation |
# =============================================================================

set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
SRC="$REPO_ROOT/scripts/hooks"
DST="$REPO_ROOT/.git/hooks"

install_hook() {
local name="$1"
if [ ! -f "$SRC/$name" ]; then
echo " ⚠ $name not found in scripts/hooks/ — skipping"
return
fi
cp "$SRC/$name" "$DST/$name"
chmod +x "$DST/$name"
echo " ✓ Installed $name"
}

echo ""
echo "Installing PatronAI git hooks..."
echo ""
install_hook "pre-push"
echo ""
echo "Done. Every 'git push' will now run:"
echo " [1/3] Code quality check"
echo " [2/3] Structural model check"
echo " [3/3] GPT-5.5 vulnerability scan"
echo " [async] PR review → GitHub comments"
echo ""
echo "Requires OPENAI_API_KEY in .env and 'gh' CLI authenticated."
echo ""
Loading
Loading