From 46319a53ff24e125ef314d4b010d31a4acf2fb3d Mon Sep 17 00:00:00 2001 From: test Date: Wed, 15 Apr 2026 20:08:35 +0200 Subject: [PATCH] Add grammar security audit and enhance vendor script - vendor-grammar.sh: copy extra headers (.h, .inc) and common/ subdirs from grammar src/ directories. Needed for Astro (tag.h), PureScript/ Typst (unicode.h), VHDL (.h/.inc files), F# (common/scanner.h). - audit-grammar-security.sh: pre-vendoring scanner for dangerous patterns in vendored grammar C files. Checks for dangerous includes (sys/*, unistd.h, dlfcn.h), dangerous calls (system, exec, popen, fopen, socket, getenv, fork, dlopen), and suspicious patterns (constructor attributes, inline assembly, base64 blobs). PASS/WARN/BLOCK per grammar. --- scripts/audit-grammar-security.sh | 105 ++++++++++++++++++++++++++++++ scripts/vendor-grammar.sh | 11 ++++ 2 files changed, 116 insertions(+) create mode 100755 scripts/audit-grammar-security.sh diff --git a/scripts/audit-grammar-security.sh b/scripts/audit-grammar-security.sh new file mode 100755 index 000000000..6cc335fc1 --- /dev/null +++ b/scripts/audit-grammar-security.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +# audit-grammar-security.sh — Scan vendored tree-sitter grammar scanner.c +# files for dangerous patterns that could indicate malicious code. +# +# Grammars are third-party C source compiled into our binary. parser.c files +# are auto-generated (low risk), but scanner.c files are hand-written C with +# access to memory allocation and input text. +# +# Usage: scripts/audit-grammar-security.sh [directory] +# Default: internal/cbm/vendored/grammars/ + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +GRAMMAR_DIR="${1:-$ROOT/internal/cbm/vendored/grammars}" + +if [ ! -d "$GRAMMAR_DIR" ]; then + echo "ERROR: directory not found: $GRAMMAR_DIR" >&2 + exit 1 +fi + +echo "=== Grammar Security Audit ===" +echo "Scanning: $GRAMMAR_DIR" +echo "" + +TOTAL=0 +WARNED=0 +BLOCKED=0 + +# Safe includes that scanners legitimately use +SAFE_INCLUDES='string\.h|stdint\.h|stdbool\.h|stdlib\.h|wctype\.h|stdio\.h|stddef\.h|limits\.h|assert\.h|ctype\.h|wchar\.h|math\.h|stdalign\.h|stdarg\.h|float\.h|inttypes\.h' + +# Dangerous includes +DANGER_INCLUDES='unistd\.h|sys/|netdb\.h|dlfcn\.h|signal\.h|spawn\.h|pthread\.h|fcntl\.h|dirent\.h|termios\.h|arpa/|netinet/' + +# Dangerous function calls +DANGER_CALLS='system\s*\(|exec[lvpe]+\s*\(|popen\s*\(|fopen\s*\(|fwrite\s*\(|socket\s*\(|getenv\s*\(|fork\s*\(|dlopen\s*\(|connect\s*\(|bind\s*\(|listen\s*\(|accept\s*\(|sendto\s*\(|recvfrom\s*\(|mmap\s*\(|mprotect\s*\(' + +# Suspicious patterns +SUSPICIOUS='__attribute__\s*\(\s*\(\s*constructor|__attribute__\s*\(\s*\(\s*destructor|asm\s*\(|__asm__|__asm\b' + +for grammar_dir in "$GRAMMAR_DIR"/*/; do + name=$(basename "$grammar_dir") + TOTAL=$((TOTAL + 1)) + issues="" + + for src in "$grammar_dir"scanner.c "$grammar_dir"scanner.cc "$grammar_dir"*.h "$grammar_dir"*.inc; do + [ -f "$src" ] || continue + basename_src=$(basename "$src") + + # Skip tree_sitter/ subdirectory headers (those are standard) + case "$src" in + */tree_sitter/*) continue ;; + esac + + # Check dangerous includes + while IFS= read -r line; do + issues="${issues} WARN $basename_src: dangerous include: $line\n" + done < <(grep -nE "#include\s*<($DANGER_INCLUDES)" "$src" 2>/dev/null || true) + + # Check dangerous calls + while IFS= read -r line; do + issues="${issues} BLOCK $basename_src: dangerous call: $line\n" + done < <(grep -nE "$DANGER_CALLS" "$src" 2>/dev/null || true) + + # Check suspicious patterns + while IFS= read -r line; do + issues="${issues} BLOCK $basename_src: suspicious pattern: $line\n" + done < <(grep -nE "$SUSPICIOUS" "$src" 2>/dev/null || true) + + # Check for base64-like long encoded strings (40+ alphanumeric chars) + while IFS= read -r line; do + issues="${issues} WARN $basename_src: possible encoded data: $line\n" + done < <(grep -nE '"[A-Za-z0-9+/]{60,}={0,2}"' "$src" 2>/dev/null || true) + done + + if [ -n "$issues" ]; then + if echo -e "$issues" | grep -q "BLOCK"; then + echo "BLOCK $name:" + echo -e "$issues" + BLOCKED=$((BLOCKED + 1)) + else + echo "WARN $name:" + echo -e "$issues" + WARNED=$((WARNED + 1)) + fi + fi +done + +echo "────────────────────────────────────────────" +echo " Scanned: $TOTAL grammars" +echo " Clean: $((TOTAL - WARNED - BLOCKED))" +echo " Warned: $WARNED" +echo " Blocked: $BLOCKED" +echo "────────────────────────────────────────────" + +if [ "$BLOCKED" -gt 0 ]; then + echo "" + echo "FAILED: $BLOCKED grammar(s) have dangerous patterns. Review before vendoring." + exit 1 +fi + +echo "" +echo "PASSED: No dangerous patterns found." +exit 0 diff --git a/scripts/vendor-grammar.sh b/scripts/vendor-grammar.sh index 06db85e2f..83009f2ff 100755 --- a/scripts/vendor-grammar.sh +++ b/scripts/vendor-grammar.sh @@ -48,6 +48,17 @@ if [ -d "$SRC_DIR/tree_sitter" ]; then cp "$SRC_DIR/tree_sitter/"*.h "$GRAMMAR_DIR/tree_sitter/" 2>/dev/null || true fi +# Copy any extra headers (.h, .inc files) used by scanners +# Examples: tag.h (Vue/Svelte/Astro), unicode.h (PureScript/Typst), +# TokenTree.h/.inc (VHDL) +for f in "$SRC_DIR"/*.h "$SRC_DIR"/*.inc; do + [ -f "$f" ] && cp "$f" "$GRAMMAR_DIR/" +done +# Copy common/ subdirectory if present (e.g., F# scanner uses common/scanner.h) +if [ -d "$SRC_DIR/common" ]; then + cp -r "$SRC_DIR/common" "$GRAMMAR_DIR/" +fi + # Copy LICENSE file from upstream repo REPO_ROOT="$TMPDIR/repo" if [ -n "$SUBDIR" ]; then