#!/usr/bin/env python3
"""
Scan Ivanti Sentry Apache and Tomcat logs for CVE-2026-10520 exploitation attempts.
Matches POST requests to the unauthenticated handleMessage endpoint or occurrences
of commandexec/reqandres payload keywords in web/application server log directories.
Exit Codes:
0: Clean (no indicators found)
1: Compromise (exploitation indicators found)
2: Execution error
"""
import os
import sys
import argparse
from pathlib import Path
# Known indicators associated with CVE-2026-10520
TARGET_ENDPOINTS = [
"/mics/api/v2/sentry/mics-config/handleMessage",
"/api/v2/sentry/mics-config/handleMessage"
]
PAYLOAD_KEYWORDS = [
"commandexec",
"reqandres",
"/configuration/system/commandexec"
]
def scan_file(file_path: Path) -> list:
"""Scan a single log file for indicators."""
matches = []
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
for line_no, line in enumerate(f, 1):
# Check for POST requests targeting the handleMessage endpoint
for endpoint in TARGET_ENDPOINTS:
if "POST" in line and endpoint in line:
matches.append({
"type": "endpoint_access",
"file": str(file_path),
"line_number": line_no,
"content": line.strip(),
"matched": endpoint
})
# Check for XML payload keywords (if full requests/payloads are logged)
for keyword in PAYLOAD_KEYWORDS:
if keyword in line:
# Exclude self-references or typical log headers if any
matches.append({
"type": "payload_keyword",
"file": str(file_path),
"line_number": line_no,
"content": line.strip(),
"matched": keyword
})
except Exception as e:
print(f"[-] Warning: Failed to read {file_path}: {e}", file=sys.stderr)
return []
return matches
def main():
parser = argparse.ArgumentParser(
description="Scan logs for Ivanti Sentry CVE-2026-10520 RCE exploitation indicators."
)
parser.add_argument(
"--log-dir",
type=str,
default="/var/log",
help="Path to the directory containing Sentry logs (e.g. /var/log/httpd or /var/log/tomcat2)"
)
args = parser.parse_args()
log_path = Path(args.log_dir)
if not log_path.exists():
print(f"[-] Error: Log directory '{log_path}' does not exist.", file=sys.stderr)
return 2
if not log_path.is_dir():
print(f"[-] Error: Log path '{log_path}' is not a directory.", file=sys.stderr)
return 2
print(f"[+] Starting log scan in: {log_path.resolve()}")
all_matches = []
# We walk the directory to scan files.
# Exclude binary or compressed logs (.gz, .zip, etc.) to keep scanning simple and safe.
for root, _, files in os.walk(log_path):
for file in files:
file_path = Path(root) / file
# Skip compressed log files
if file_path.suffix in [".gz", ".zip", ".tar", ".tgz"]:
continue
# Scan matching log filename patterns or scan all text logs
if any(pat in file for pat in ["access_log", "localhost_access", "catalina", "httpd", "portal", "message"]):
print(f"[+] Scanning: {file_path}")
matches = scan_file(file_path)
if matches:
all_matches.extend(matches)
if all_matches:
print("\n[!] SUSPICIOUS LOG ENTRIES DETECTED:")
for match in all_matches:
print(f" [{match['type'].upper()}] File: {match['file']}:{match['line_number']}")
print(f" Match: {match['matched']}")
print(f" Line: {match['content']}")
return 1
print("[+] Scan completed. No indicators of CVE-2026-10520 found.")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
print(f"[-] Execution failure: {e}", file=sys.stderr)
sys.exit(2)