#!/usr/bin/env python3
# catscan.py — cat كامل + ماسح ثغرات مدمج
# run: python3 catscan.py [options] [files...]
#      python3 catscan.py --scan /path/to/project
#      python3 catscan.py -n file.txt
# python3.8+

import sys
import os
import re
import argparse
import subprocess
from pathlib import Path

# ════════════════════════════════════════════════════════════
#  الجزء الأول: cat الكامل
# ════════════════════════════════════════════════════════════

def cat_files(files, number=False, number_nonblank=False,
              show_ends=False, show_tabs=False, squeeze=False,
              show_nonprinting=False, show_all=False):
    """عرض ملفات مع خيارات cat الكاملة."""
    if show_all:
        show_nonprinting = show_ends = show_tabs = True
    line_num = 0
    for fname in files:
        try:
            if fname == "-":
                data = sys.stdin.read()
                _emit(data, number, number_nonblank, show_ends,
                      show_tabs, squeeze, show_nonprinting, line_num)
                continue
            with open(fname, "r", encoding="utf-8", errors="replace") as fh:
                data = fh.read()
        except FileNotFoundError:
            print(f"catscan: {fname}: No such file or directory", file=sys.stderr)
            continue
        except IsADirectoryError:
            print(f"catscan: {fname}: Is a directory", file=sys.stderr)
            continue
        except PermissionError:
            print(f"catscan: {fname}: Permission denied", file=sys.stderr)
            continue
        line_num = _emit(data, number, number_nonblank, show_ends,
                         show_tabs, squeeze, show_nonprinting, line_num)


def _emit(data, number, number_nonblank, show_ends,
          show_tabs, squeeze, show_nonprinting, line_num):
    lines = data.split("\n")
    blank_prev = False
    for i, line in enumerate(lines):
        is_last = (i == len(lines) - 1)
        if is_last and line == "":
            break
        blank = (line.strip() == "")
        if squeeze and blank and blank_prev:
            continue
        blank_prev = blank
        out = line
        if show_tabs:
            out = out.replace("\t", "^I")
        if show_nonprinting:
            out = "".join(
                c if (32 <= ord(c) < 127 or c == "\t") else
                f"M-{chr(ord(c) - 128)}" if ord(c) >= 128 else f"^{chr(ord(c)+64)}"
                for c in out
            )
        if number_nonblank and not blank:
            line_num += 1
            out = f"{line_num:6d}\t{out}"
        elif number and not number_nonblank:
            line_num += 1
            out = f"{line_num:6d}\t{out}"
        if show_ends:
            out = out + "$"
        print(out)
    return line_num


# ════════════════════════════════════════════════════════════
#  الجزء الثاني: قاعدة أنماط الثغرات
# ════════════════════════════════════════════════════════════

PATTERNS = [
    # (التصنيف, الخطورة, regex, وصف)
    ("أسرار مكشوفة", "HIGH",
     r"(?i)(password|passwd|pwd)\s*[=:]\s*['\"][^'\"]{3,}['\"]",
     "كلمة مرور مكتوبة مباشرة"),
    ("أسرار مكشوفة", "HIGH",
     r"(?i)(api[_-]?key|apikey|secret[_-]?key)\s*[=:]\s*['\"][^'\"]{8,}['\"]",
     "مفتاح API مكتوب مباشرة"),
    ("أسرار مكشوفة", "HIGH",
     r"(?i)(aws_access_key_id|aws_secret_access_key)\s*[=:]\s*['\"]?[A-Z0-9]{16,}",
     "مفتاح AWS مكشوف"),
    ("أسرار مكشوفة", "HIGH",
     r"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
     "مفتاح SSH خاص مكشوف"),
    ("أسرار مكشوفة", "MED",
     r"(?i)(token|bearer)\s*[=:]\s*['\"][A-Za-z0-9_\-\.]{20,}['\"]",
     "توكن مكشوف"),

    ("حقن SQL", "HIGH",
     r"(?i)(execute|query|cursor\.execute)\s*\(\s*['\"].*%s.*['\"]\s*%",
     "تنسيق SQL بـ %s — حقن محتمل"),
    ("حقن SQL", "HIGH",
     r"(?i)(select|insert|update|delete)\s+.*\+.*(request|input|params)",
     "SQL مبني من إدخال المستخدم"),
    ("حقن SQL", "MED",
     r"(?i)f['\"]\s*(select|insert|update|delete)\s+.*\{.*\}",
     "f-string داخل SQL — حقن محتمل"),

    ("تنفيذ أوامر", "CRIT",
     r"(?i)os\.system\s*\(|os\.popen\s*\(|subprocess\.(call|run|Popen).*shell\s*=\s*True",
     "تنفيذ أمر نظام — خطر حقن أوامر"),
    ("تنفيذ أوامر", "CRIT",
     r"(?i)\beval\s*\(|\bexec\s*\(",
     "eval/exec — تنفيذ كود ديناميكي"),
    ("تنفيذ أوامر", "HIGH",
     r"(?i)subprocess\.(call|run|Popen)\s*\(.*\+",
     "بناء أمر من نص متغير"),

    ("XSS", "HIGH",
     r"(?i)(innerHTML|outerHTML)\s*=",
     "إدخال HTML مباشر — XSS"),
    ("XSS", "HIGH",
     r"(?i)document\.write\s*\(",
     "document.write — XSS"),
    ("XSS", "MED",
     r"(?i)(dangerouslySetInnerHTML|v-html)",
     "HTML خام في إطار عمل"),

    ("تضمين ملفات", "HIGH",
     r"(?i)(include|require)(_once)?\s*\(\s*\$_(GET|POST|REQUEST)",
     "تضمين ملفات من إدخال المستخدم — LFI/RFI"),
    ("تضمين ملفات", "HIGH",
     r"(?i)(readfile|file_get_contents|fopen)\s*\(\s*\$_(GET|POST)",
     "قراءة ملف من إدخال المستخدم"),

    ("اجتياز مسار", "HIGH",
     r"(?i)(open|read|load)\s*\(.*(request|input|params|args)",
     "فتح ملف من مدخل غير موثوق — Path Traversal"),

    ("إلغاء تسلسل", "CRIT",
     r"(?i)(pickle\.loads|yaml\.load\s*\(|marshal\.loads|unserialize\s*\()",
     "إلغاء تسلسل غير آمن — RCE محتمل"),

    ("تشفير ضعيف", "MED",
     r"(?i)\b(md5|sha1)\s*\(",
     "تجزئة ضعيفة md5/sha1"),
    ("تشفير ضعيف", "HIGH",
     r"(?i)(DES|RC4|ECB)\b",
     "خوارزمية تشفير ضعيفة"),
    ("تشفير ضعيف", "MED",
     r"(?i)random\.(random|randint)\s*\(",
     "عشوائية غير آمنة — استخدم secrets"),

    ("تحقق معطّل", "HIGH",
     r"(?i)verify\s*=\s*False",
     "التحقق من شهادة TLS معطّل"),
    ("تحقق معطّل", "CRIT",
     r"(?i)(rejectUnauthorized\s*:\s*false|InsecureSkipVerify\s*:\s*true)",
     "تجاهل شهادة TLS"),
    ("تحقق معطّل", "MED",
     r"(?i)chmod\s*\(.*0o?777",
     "صلاحيات 777 — وصول كامل"),

    ("تسريب معلومات", "MED",
     r"(?i)(debug\s*=\s*True|DEBUG\s*=\s*True)",
     "وضع التصحيح مفعّل"),
    ("تسريب معلومات", "MED",
     r"(?i)(traceback\.print_exc|print\s*\(\s*e\s*\))",
     "طباعة استثناء — تسريب معلومات"),
    ("تسريب معلومات", "LOW",
     r"(?i)(console\.log|print)\s*\(.*(password|token|secret)",
     "طباعة بيانات حساسة"),

    ("SSRF", "HIGH",
     r"(?i)(requests\.(get|post)|urllib\.request|curl)\s*\(\s*(request|input|args|params)",
     "طلب HTTP من مدخل المستخدم — SSRF"),

    ("CSRF", "MED",
     r"(?i)@app\.route.*methods.*POST",
     "مسار POST — تحقق من حماية CSRF"),

    ("تسريب CORS", "HIGH",
     r"(?i)Access-Control-Allow-Origin\s*[:=]\s*['\"]?\*",
     "CORS مفتوح للجميع"),

    ("تسريب CORS", "MED",
     r"(?i)(Access-Control-Allow-Credentials\s*[:=]\s*true)",
     "CORS مع credentials"),

    ("حقن LDAP", "HIGH",
     r"(?i)(ldap_search|ldap_bind)\s*\(.*\+",
     "بناء استعلام LDAP من نص"),
]

# امتدادات الملفات النصية
TEXT_EXTS = {
    ".py", ".js", ".ts", ".jsx", ".tsx", ".php", ".rb", ".go",
    ".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".rs", ".sh",
    ".bash", ".zsh", ".ps1", ".sql", ".html", ".htm", ".xml",
    ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
    ".env", ".txt", ".md", ".dockerfile", ".tf", ".vue", ".svelte",
}

SKIP_DIRS = {
    ".git", "node_modules", "__pycache__", ".venv", "venv",
    "dist", "build", "target", ".idea", ".vscode", "vendor",
    ".next", ".nuxt", "coverage", ".pytest_cache", "site-packages",
}


# ════════════════════════════════════════════════════════════
#  الجزء الثالث: الماسح
# ════════════════════════════════════════════════════════════

SEVERITY_ORDER = {"CRIT": 0, "HIGH": 1, "MED": 2, "LOW": 3}
SEVERITY_COLOR = {
    "CRIT": "\033[1;35m",  # بنفسجي
    "HIGH": "\033[1;31m",  # أحمر
    "MED":  "\033[1;33m",  # أصفر
    "LOW":  "\033[1;36m",  # سماوي
}
RESET = "\033[0m"
BOLD = "\033[1m"


def scan_file(path: Path):
    """مسح ملف واحد وإرجاع قائمة النتائج."""
    findings = []
    try:
        with open(path, "r", encoding="utf-8", errors="replace") as fh:
            lines = fh.readlines()
    except (PermissionError, OSError):
        return findings

    compiled = [(cat, sev, re.compile(pat), desc) for cat, sev, pat, desc in PATTERNS]
    for lineno, line in enumerate(lines, 1):
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        if len(line) > 2000:
            continue
        for cat, sev, rx, desc in compiled:
            if rx.search(line):
                findings.append({
                    "file": str(path),
                    "line": lineno,
                    "category": cat,
                    "severity": sev,
                    "desc": desc,
                    "code": stripped[:120],
                })
    return findings


def walk_target(root: Path):
    """المشي على مجلد وإرجاع الملفات النصية."""
    files = []
    if root.is_file():
        return [root]
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fn in filenames:
            p = Path(dirpath) / fn
            if p.suffix.lower() in TEXT_EXTS or fn in (
                "Dockerfile", "Makefile", ".env", ".gitignore"):
                files.append(p)
    return files


def print_findings(findings):
    """طباعة النتائج مرتبة حسب الخطورة."""
    if not findings:
        print(f"{BOLD}لا ثغرات مطابقة.{RESET}")
        return

    # إزالة التكرارات (نفس الملف والسطر والتصنيف)
    seen = set()
    unique = []
    for f in findings:
        key = (f["file"], f["line"], f["category"])
        if key not in seen:
            seen.add(key)
            unique.append(f)

    unique.sort(key=lambda x: (SEVERITY_ORDER[x["severity"]], x["file"], x["line"]))

    print(f"\n{BOLD}{'═'*60}{RESET}")
    print(f"{BOLD}  نتائج المسح — {len(unique)} إصابة{RESET}")
    print(f"{BOLD}{'═'*60}{RESET}\n")

    # ملخص
    counts = {}
    for f in unique:
        counts[f["severity"]] = counts.get(f["severity"], 0) + 1
    for sev in ("CRIT", "HIGH", "MED", "LOW"):
        if sev in counts:
            c = SEVERITY_COLOR[sev]
            print(f"  {c}{sev:5s}{RESET} : {counts[sev]}")
    print()

    # التفاصيل
    for f in unique:
        c = SEVERITY_COLOR[f["severity"]]
        print(f"{c}[{f['severity']}]{RESET} {BOLD}{f['category']}{RESET}")
        print(f"  {f['file']}:{f['line']}")
        print(f"  {f['desc']}")
        print(f"  > {f['code']}")
        print()


def scan_target(target: str, quiet=False):
    """نقطة الدخول للمسح."""
    root = Path(target)
    if not root.exists():
        print(f"catscan: {target}: لا يوجد", file=sys.stderr)
        return
    files = walk_target(root)
    if not quiet:
        print(f"[*] مسح {len(files)} ملف في {target} ...")
    all_findings = []
    for p in files:
        all_findings.extend(scan_file(p))
    print_findings(all_findings)


# ════════════════════════════════════════════════════════════
#  الجزء الرابع: واجهة الأوامر
# ════════════════════════════════════════════════════════════

def main():
    ap = argparse.ArgumentParser(
        prog="catscan",
        description="cat كامل + ماسح ثغرات مدمج",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
أمثلة:
  catscan file.txt                  # عرض ملف
  catscan -n file.txt               # عرض مع أرقام الأسطر
  catscan -A file.txt               # عرض مع إظهار المحارف الخاصة
  catscan --scan /path/to/project   # مسح مشروع بحثاً عن ثغرات
  catscan --scan . --quiet          # مسح بدون رسائل
  catscan -n file1.txt file2.txt    # عرض عدة ملفات
        """)

    # خيارات cat
    cat_group = ap.add_argument_group("خيارات cat")
    cat_group.add_argument("-n", "--number", action="store_true",
                           help="ترقيم جميع أسطر الإخراج")
    cat_group.add_argument("-b", "--number-nonblank", action="store_true",
                           help="ترقيم الأسطر غير الفارغة فقط")
    cat_group.add_argument("-E", "--show-ends", action="store_true",
                           help="إظهار $ في نهاية كل سطر")
    cat_group.add_argument("-T", "--show-tabs", action="store_true",
                           help="إظهار التبويبات كـ ^I")
    cat_group.add_argument("-v", "--show-nonprinting", action="store_true",
                           help="إظهار المحارف غير القابلة للطباعة")
    cat_group.add_argument("-A", "--show-all", action="store_true",
                           help="مكافئ لـ -vET")
    cat_group.add_argument("-s", "--squeeze-blank", action="store_true",
                           help="ضغط الأسطر الفارغة المتكررة")

    # خيارات المسح
    scan_group = ap.add_argument_group("خيارات المسح")
    scan_group.add_argument("--scan", metavar="PATH",
                            help="مسح مجلد أو ملف بحثاً عن ثغرات")
    scan_group.add_argument("--quiet", action="store_true",
                            help="كتم رسائل المسح")

    ap.add_argument("files", nargs="*", help="ملفات للعرض")

    args = ap.parse_args()

    # وضع المسح
    if args.scan:
        scan_target(args.scan, quiet=args.quiet)
        return

    # وضع cat
    if not args.files:
        # قراءة من stdin
        data = sys.stdin.read()
        _emit(data, args.number, args.number_nonblank, args.show_ends,
              args.show_tabs, args.squeeze_blank,
              args.show_nonprinting, args.show_all, 0)
        return

    cat_files(
        args.files,
        number=args.number,
        number_nonblank=args.number_nonblank,
        show_ends=args.show_ends,
        show_tabs=args.show_tabs,
        squeeze=args.squeeze_blank,
        show_nonprinting=args.show_nonprinting,
        show_all=args.show_all,
    )


if __name__ == "__main__":
    main()
