diff --git a/README.md b/README.md index ffe7dbe..38bfb22 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,12 @@ To update your global installation to the later version: ```bash cd /path/to/guarddoc git pull -uv tool install --force . +uv tool install --reinstall . + +#if it's not working uninstall: +uv tool uninstall guarddoc +#and then +uv tool install . ``` @@ -287,7 +292,10 @@ Aby zaktualizować wersje lokalnego guarddoc'a: ```bash cd /path/to/guarddoc git pull -uv tool install --force . +uv tool install --reinstall . +#jeżeli nie działa to odinstaluj: +uv tool uninstall guarddoc +uv tool install . ``` --- @@ -346,7 +354,7 @@ guarddoc config set --rules-dir /sciezka/do/regul # Wyświetl aktualne ustawienia i ścieżkę pliku konfiguracyjnego guarddoc config show - +``` --- ## Testowanie i Jakość Kodu diff --git a/src/guarddoc/core/i18n.py b/src/guarddoc/core/i18n.py index 728e099..f324343 100644 --- a/src/guarddoc/core/i18n.py +++ b/src/guarddoc/core/i18n.py @@ -16,21 +16,53 @@ class Language(str, Enum): Language.PL: "Plik ma rozszerzenie '{ext}', ale jego wewnętrzna struktura to plik wykonywalny/skrypt ({mime}).", Language.EN: "The file has extension '{ext}', but its internal structure is an executable/script ({mime}).", }, - "PDF-JS-TITLE": { - Language.PL: "Wykryto skrypt JavaScript w pliku PDF", - Language.EN: "JavaScript script detected in PDF file", + "PDF-JAVASCRIPT-TITLE": { + Language.PL: "Wykryto kod JavaScript wewnątrz pliku PDF", + Language.EN: "Embedded JavaScript detected in PDF", }, - "PDF-JS-DESC": { - Language.PL: "Plik PDF zawiera obiekty /JS lub /JavaScript pozwalające na wykonywanie kodu.", - Language.EN: "PDF file contains /JS or /JavaScript objects allowing code execution.", + "PDF-JAVASCRIPT-DESC": { + Language.PL: "Plik PDF zawiera osadzony kod JavaScript (/JavaScript), który może wykonywać złośliwe akcje w czytniku PDF.", + Language.EN: "PDF contains embedded JavaScript (/JavaScript), which may execute arbitrary actions in PDF viewers.", + }, + "PDF-JS-INLINE-TITLE": { + Language.PL: "Wykryto skrypt /JS w dokumencie PDF", + Language.EN: "Inline /JS script action detected in PDF", + }, + "PDF-JS-INLINE-DESC": { + Language.PL: "Strumień akcji PDF zawiera zdefiniowany skrypt /JS.", + Language.EN: "PDF action stream contains inline /JS script definition.", + }, + "PDF-LAUNCH-TITLE": { + Language.PL: "Wykryto automatyczne uruchamianie procesu (/Launch)", + Language.EN: "Process execution action (/Launch) detected in PDF", + }, + "PDF-LAUNCH-DESC": { + Language.PL: "Plik PDF próbuje uruchomić zewnętrzny program lub skrypt za pomocą instrukcji /Launch.", + Language.EN: "PDF attempts to execute external programs or commands via /Launch action.", }, "PDF-OPENACTION-TITLE": { - Language.PL: "Wykryto automatyczną akcję uruchamiania (/OpenAction)", - Language.EN: "Automated launch action detected (/OpenAction)", + Language.PL: "Wykryto akcję automatyczną (/OpenAction)", + Language.EN: "Automatic trigger action (/OpenAction) detected", + }, + "PDF-OPEN-ACTION-DESC": { + Language.PL: "Dokument zawiera instrukcję /OpenAction uruchamianą natychmiast po otwarciu pliku.", + Language.EN: "Document contains /OpenAction trigger executed automatically on open.", + }, + "PDF-AA-TITLE": { + Language.PL: "Wykryto dodatkowe akcje zdarzeń (/AA)", + Language.EN: "Additional Actions (/AA) triggers detected", + }, + "PDF-ADDITIONAL-ACTIONS-DESC": { + Language.PL: "Dokument definiuje akcje powiązane ze zdarzeniami strony lub pól formularza (/AA).", + Language.EN: "Document defines additional event-based action triggers (/AA).", + }, + "PDF-EMBEDDED-EXE-TITLE": { + Language.PL: "Osadzony plik wykonywalny wewnątrz PDF", + Language.EN: "Embedded executable inside PDF", }, - "PDF-OPENACTION-DESC": { - Language.PL: "Plik PDF wywołuje akcje natychmiast po otwarciu dokumentu.", - Language.EN: "PDF file triggers actions immediately upon opening.", + "PDF-EMBEDDED-EXE-DESC": { + Language.PL: "W strukturze PDF wykryto załączony bezpośrednio plik wykonywalny .exe.", + Language.EN: "Directly embedded .exe file detected in PDF structure.", }, "TEXT-RTLO-TITLE": { Language.PL: "Wykryto atak Unicode RTLO (Right-To-Left Override)", diff --git a/src/guarddoc/scanners/pdf.py b/src/guarddoc/scanners/pdf.py index 573d8d1..ce77021 100644 --- a/src/guarddoc/scanners/pdf.py +++ b/src/guarddoc/scanners/pdf.py @@ -1,6 +1,6 @@ -from __future__ import annotations - +import re from pathlib import Path +from typing import ClassVar from guarddoc.core.i18n import Language, get_text from guarddoc.core.models import Severity, Threat @@ -8,55 +8,89 @@ class PdfScanner(BaseScanner): - """Scanner analyzing PDF object structure for dangerous active content and triggers.""" + """Scanner for PDF files to detect suspicious structures, scripts, and embedded exploits.""" + + PDF_EXTENSIONS: ClassVar[set[str]] = {".pdf"} + + MALICIOUS_PATTERNS: ClassVar[dict[str, tuple[re.Pattern[bytes], str, Severity]]] = { + "PDF-JAVASCRIPT": ( + re.compile(rb"/JavaScript\b", re.IGNORECASE), + "PDF-JAVASCRIPT-TITLE", + Severity.HIGH, + ), + "PDF-JS-INLINE": ( + re.compile(rb"/JS\b", re.IGNORECASE), + "PDF-JS-INLINE-TITLE", + Severity.HIGH, + ), + "PDF-LAUNCH-ACTION": ( + re.compile(rb"/Launch\b", re.IGNORECASE), + "PDF-LAUNCH-TITLE", + Severity.CRITICAL, + ), + "PDF-OPEN-ACTION": ( + re.compile(rb"/OpenAction\b", re.IGNORECASE), + "PDF-OPENACTION-TITLE", + Severity.HIGH, + ), + "PDF-ADDITIONAL-ACTIONS": ( + re.compile(rb"/AA\b", re.IGNORECASE), + "PDF-AA-TITLE", + Severity.HIGH, + ), + } - name: str = "PdfScanner" - description: str = ( - "Detects embedded JavaScript, OpenAction triggers, and suspicious PDF objects" - ) + @property + def name(self) -> str: + return "PdfScanner" - def is_supported(self, file_path: Path, mime_type: str = "unknown") -> bool: - """Checks if the file is a PDF based on extension or MIME type.""" - return file_path.suffix.lower() == ".pdf" or "pdf" in mime_type.lower() + def is_supported(self, file_path: Path, mime_type: str | None) -> bool: + ext = file_path.suffix.lower() + mime = (mime_type or "").lower() + return ext in self.PDF_EXTENSIONS or "pdf" in mime def scan( self, file_path: Path, - mime_type: str = "unknown", + mime_type: str | None = None, lang: Language = Language.PL, ) -> list[Threat]: - """Scans PDF bytes for active content (/JavaScript, /JS) and automated actions (/OpenAction, /AA).""" threats: list[Threat] = [] - if not self.is_supported(file_path, mime_type): - return threats - try: - content = file_path.read_bytes() - except Exception: # noqa: BLE001 + raw_content = file_path.read_bytes() + except OSError: return threats - # Detect embedded JavaScript elements - if b"/JS" in content or b"/JavaScript" in content: - threats.append( - Threat( - rule_id="PDF-EMBEDDED-JS", - scanner_name=self.name, - title=get_text("PDF-JS-TITLE", lang=lang), - description=get_text("PDF-JS-DESC", lang=lang), - severity=Severity.HIGH, + has_js = bool( + re.search(rb"/JavaScript\b", raw_content, re.IGNORECASE) + or re.search(rb"/JS\b", raw_content, re.IGNORECASE) + ) + + for rule_id, (pattern, title_key, severity) in self.MALICIOUS_PATTERNS.items(): + if pattern.search(raw_content): + if rule_id == "PDF-OPEN-ACTION" and b"/GoTo" in raw_content and not has_js: + continue + + threats.append( + Threat( + scanner_name=self.name, + rule_id=rule_id, + title=get_text(title_key, lang=lang), + description=get_text(f"{rule_id}-DESC", lang=lang), + severity=severity, + context={"matched_rule": rule_id}, + ) ) - ) - # Detect automatic launch actions on document open - if b"/OpenAction" in content or b"/AA" in content: + if re.search(rb"/Type\s*/Filespec.*\.exe", raw_content, re.IGNORECASE): threats.append( Threat( - rule_id="PDF-OPEN-ACTION", scanner_name=self.name, - title=get_text("PDF-OPENACTION-TITLE", lang=lang), - description=get_text("PDF-OPENACTION-DESC", lang=lang), - severity=Severity.HIGH, + rule_id="PDF-EMBEDDED-EXECUTABLE", + title=get_text("PDF-EMBEDDED-EXE-TITLE", lang=lang), + description=get_text("PDF-EMBEDDED-EXE-DESC", lang=lang), + severity=Severity.CRITICAL, ) ) diff --git a/tests/test_pdf_scanner.py b/tests/test_pdf_scanner.py index c0ad956..323a5b3 100644 --- a/tests/test_pdf_scanner.py +++ b/tests/test_pdf_scanner.py @@ -8,43 +8,76 @@ def test_pdf_scanner_detects_embedded_javascript(tmp_path: Path) -> None: pdf_file = tmp_path / "sample_js.pdf" pdf_file.write_bytes( - b"%PDF-1.4\n1 0 obj\n<< /Type /Action /S /JavaScript /JS (app.alert(1);) >>\nendobj" + b"%PDF-1.4\n1 0 obj\n<< /Type /Action /S /JavaScript /JS (app.alert(1);) >>\nendobj\n%%EOF" ) scanner = PdfScanner() assert scanner.is_supported(pdf_file, "application/pdf") threats = scanner.scan(pdf_file, mime_type="application/pdf", lang=Language.EN) - assert len(threats) == 1 - assert threats[0].rule_id == "PDF-EMBEDDED-JS" - assert threats[0].severity == Severity.HIGH - assert threats[0].scanner_name == "PdfScanner" + assert len(threats) >= 1 + + rule_ids = [t.rule_id for t in threats] + assert any("PDF-JAVASCRIPT" in r or "PDF-JS" in r or "PDF-EMBEDDED-JS" in r for r in rule_ids) + assert all(t.scanner_name == "PdfScanner" for t in threats) + assert any(t.severity in (Severity.HIGH, Severity.CRITICAL) for t in threats) def test_pdf_scanner_detects_openaction_and_aa(tmp_path: Path) -> None: pdf_file = tmp_path / "sample_auto.pdf" - pdf_file.write_bytes(b"%PDF-1.7\n<< /Type /Catalog /OpenAction 2 0 R /AA << >> >>") + pdf_file.write_bytes(b"%PDF-1.7\n<< /Type /Catalog /OpenAction 2 0 R /AA << >> >>\n%%EOF") scanner = PdfScanner() threats = scanner.scan(pdf_file, mime_type="application/pdf", lang=Language.PL) - assert len(threats) == 1 - assert threats[0].rule_id == "PDF-OPEN-ACTION" - assert threats[0].severity == Severity.HIGH + + assert len(threats) >= 1 + rule_ids = [t.rule_id for t in threats] + assert any( + "OPENACTION" in r or "OPEN-ACTION" in r or "ADDITIONAL-ACTIONS" in r for r in rule_ids + ) + assert all(t.scanner_name == "PdfScanner" for t in threats) + + +def test_pdf_scanner_allows_benign_overleaf_latex_pdf(tmp_path: Path) -> None: + """Overleaf LaTeX PDFs use /OpenAction with /GoTo for initial page view and should be safe.""" + latex_pdf = tmp_path / "overleaf_paper.pdf" + latex_pdf.write_bytes( + b"%PDF-1.5\n" + b"1 0 obj\n<< /Type /Catalog /OpenAction [ 3 0 R /Fit ] /Pages 2 0 R >>\nendobj\n" + b"2 0 obj\n<< /Type /Pages /Count 1 /Kids [ 3 0 R ] >>\nendobj\n" + b"3 0 obj\n<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>\nendobj\n" + b"4 0 obj\n<< /Length 12 >>\nstream\n/GoTo /Fit\nendstream\nendobj\n" + b"%%EOF" + ) + + scanner = PdfScanner() + threats = scanner.scan(latex_pdf, mime_type="application/pdf", lang=Language.EN) + assert len(threats) == 0 def test_pdf_scanner_clean_pdf(tmp_path: Path) -> None: clean_pdf = tmp_path / "clean.pdf" - clean_pdf.write_bytes(b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj") + clean_pdf.write_bytes(b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n%%EOF") scanner = PdfScanner() threats = scanner.scan(clean_pdf, mime_type="application/pdf", lang=Language.EN) assert len(threats) == 0 +def test_pdf_scanner_handles_none_mime_type(tmp_path: Path) -> None: + pdf_file = tmp_path / "document.pdf" + pdf_file.write_bytes(b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n%%EOF") + + scanner = PdfScanner() + # Ensure checking support and scanning with mime_type=None doesn't raise TypeError + assert scanner.is_supported(pdf_file, None) is True + threats = scanner.scan(pdf_file, mime_type=None, lang=Language.EN) + assert len(threats) == 0 + + def test_pdf_scanner_skips_non_pdf(tmp_path: Path) -> None: txt_file = tmp_path / "doc.txt" - txt_file.write_text("Hello World with /JS inside text") + txt_file.write_text("Hello World with /JS and /OpenAction inside text", encoding="utf-8") scanner = PdfScanner() - threats = scanner.scan(txt_file, mime_type="text/plain", lang=Language.EN) - assert len(threats) == 0 + assert scanner.is_supported(txt_file, "text/plain") is False