audit_package_security
Audit AUR packages for security risks by analyzing PKGBUILD files and evaluating package metadata. Identify red flags and assess trustworthiness before installation.
Instructions
[SECURITY] Comprehensive security audit for AUR packages. Actions: pkgbuild_analysis (scan PKGBUILD for 50+ red flags), metadata_risk (evaluate trustworthiness via votes/maintainer/age). Examples: audit_package_security(action='pkgbuild_analysis', pkgbuild_content='...'), audit_package_security(action='metadata_risk', package_name='yay'). ⚠️ Always audit AUR packages before installing.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Type of security audit | |
| pkgbuild_content | No | PKGBUILD content for analysis | |
| package_name | No | Package name for metadata analysis | |
| package_info | No | Pre-fetched package metadata |
Implementation Reference
- src/arch_ops_server/aur.py:1192-1246 (handler)Main handler function for audit_package_security. Dispatches to analyze_pkgbuild_safety() for 'pkgbuild_analysis' action or analyze_package_metadata_risk() for 'metadata_risk' action. Handles parameter validation and error responses.
async def audit_package_security( action: Literal["pkgbuild_analysis", "metadata_risk"], pkgbuild_content: Optional[str] = None, package_name: Optional[str] = None, package_info: Optional[dict] = None ) -> dict: """ Unified security audit tool for AUR packages. Args: action: Type of security audit pkgbuild_content: PKGBUILD content (for pkgbuild_analysis) package_name: Package name (for metadata_risk) package_info: Pre-fetched package metadata (optional, for metadata_risk) Returns: Security analysis results """ if action == "pkgbuild_analysis": if not pkgbuild_content: return create_error_response( "pkgbuild_content is required for pkgbuild_analysis", error_type="validation_error" ) result = analyze_pkgbuild_safety(pkgbuild_content) result["action"] = "pkgbuild_analysis" return result elif action == "metadata_risk": if not package_name and not package_info: return create_error_response( "Either package_name or package_info is required for metadata_risk", error_type="validation_error" ) if package_info: result = analyze_package_metadata_risk(package_info) else: # Fetch package info first search_result = await search_aur(package_name, limit=1) if "error" in search_result or not search_result.get("results"): return create_error_response( f"Could not find package '{package_name}' in AUR", error_type="not_found" ) result = analyze_package_metadata_risk(search_result["results"][0]) result["action"] = "metadata_risk" return add_aur_warning(result) else: return create_error_response( f"Unknown action: {action}", error_type="validation_error" ) - MCP Tool registration with input schema: action (enum: pkgbuild_analysis, metadata_risk), pkgbuild_content (string), package_name (string), package_info (object). Required: action. Read-only annotation.
Tool( name="audit_package_security", description="[SECURITY] Comprehensive security audit for AUR packages. Actions: pkgbuild_analysis (scan PKGBUILD for 50+ red flags), metadata_risk (evaluate trustworthiness via votes/maintainer/age). Examples: audit_package_security(action='pkgbuild_analysis', pkgbuild_content='...'), audit_package_security(action='metadata_risk', package_name='yay'). ⚠️ Always audit AUR packages before installing.", inputSchema={ "type": "object", "properties": { "action": { "type": "string", "enum": ["pkgbuild_analysis", "metadata_risk"], "description": "Type of security audit" }, "pkgbuild_content": { "type": "string", "description": "PKGBUILD content for analysis" }, "package_name": { "type": "string", "description": "Package name for metadata analysis" }, "package_info": { "type": "object", "description": "Pre-fetched package metadata" } }, "required": ["action"] }, annotations=ToolAnnotations(readOnlyHint=True) ), - src/arch_ops_server/server.py:1135-1141 (registration)Tool dispatch handler in server.py call_tool() that receives audit_package_security calls and delegates to the handler in aur.py with extracted arguments.
elif name == "audit_package_security": action = arguments["action"] pkgbuild_content = arguments.get("pkgbuild_content", None) package_name = arguments.get("package_name", None) package_info = arguments.get("package_info", None) result = await audit_package_security(action, pkgbuild_content, package_name, package_info) return [TextContent(type="text", text=json.dumps(result, indent=2))] - src/arch_ops_server/__init__.py:19-19 (registration)Import/export of audit_package_security from aur module (also exported in __all__ at line 133).
audit_package_security, - src/arch_ops_server/aur.py:328-582 (helper)analyze_package_metadata_risk() helper: evaluates AUR package trustworthiness via votes, maintainer, age, update frequency. Returns trust_score (0-100), risk_factors, trust_indicators, and recommendation.
def analyze_package_metadata_risk(package_info: Dict[str, Any]) -> Dict[str, Any]: """ Analyze AUR package metadata for security and trustworthiness indicators. Evaluates: - Package popularity and community trust (votes) - Maintainer status (orphaned packages) - Update frequency (out-of-date, abandoned packages) - Package age and maturity - Maintainer history Args: package_info: Package info dict from AUR RPC (formatted or raw) Returns: Dict with metadata risk analysis including: - trust_score: 0-100 (higher = more trustworthy) - risk_factors: list of identified risks - trust_indicators: list of positive indicators - recommendation: trust recommendation """ from datetime import datetime, timedelta risk_factors = [] trust_indicators = [] logger.debug(f"Analyzing metadata for package: {package_info.get('name', 'unknown')}") # ======================================================================== # EXTRACT METADATA # ======================================================================== votes = package_info.get("votes", package_info.get("NumVotes", 0)) popularity = package_info.get("popularity", package_info.get("Popularity", 0.0)) maintainer = package_info.get("maintainer", package_info.get("Maintainer")) out_of_date = package_info.get("out_of_date", package_info.get("OutOfDate")) last_modified = package_info.get("last_modified", package_info.get("LastModified")) first_submitted = package_info.get("first_submitted", package_info.get("FirstSubmitted")) # ======================================================================== # ANALYZE VOTING/POPULARITY # ======================================================================== if votes == 0: risk_factors.append({ "category": "popularity", "severity": "HIGH", "issue": "Package has zero votes - untested by community" }) elif votes < 5: risk_factors.append({ "category": "popularity", "severity": "MEDIUM", "issue": f"Low vote count ({votes}) - limited community validation" }) elif votes >= 50: trust_indicators.append({ "category": "popularity", "indicator": f"High vote count ({votes}) - well-trusted by community" }) elif votes >= 20: trust_indicators.append({ "category": "popularity", "indicator": f"Moderate vote count ({votes}) - some community validation" }) # Popularity scoring if popularity < 0.001: risk_factors.append({ "category": "popularity", "severity": "MEDIUM", "issue": f"Very low popularity score ({popularity:.4f}) - rarely used" }) elif popularity >= 1.0: trust_indicators.append({ "category": "popularity", "indicator": f"High popularity score ({popularity:.2f}) - widely used" }) # ======================================================================== # ANALYZE MAINTAINER STATUS # ======================================================================== if not maintainer or maintainer == "None": risk_factors.append({ "category": "maintainer", "severity": "CRITICAL", "issue": "Package is ORPHANED - no active maintainer" }) else: trust_indicators.append({ "category": "maintainer", "indicator": f"Active maintainer: {maintainer}" }) # ======================================================================== # ANALYZE OUT-OF-DATE STATUS # ======================================================================== if out_of_date: # Check if out_of_date is a boolean or timestamp if isinstance(out_of_date, bool) and out_of_date: risk_factors.append({ "category": "maintenance", "severity": "MEDIUM", "issue": "Package is flagged as out-of-date" }) elif isinstance(out_of_date, (int, float)): # It's a timestamp try: ood_date = datetime.fromtimestamp(out_of_date) ood_days = (datetime.now() - ood_date).days risk_factors.append({ "category": "maintenance", "severity": "MEDIUM" if ood_days < 90 else "HIGH", "issue": f"Out-of-date for {ood_days} days since {ood_date.strftime('%Y-%m-%d')}" }) except Exception: risk_factors.append({ "category": "maintenance", "severity": "MEDIUM", "issue": "Package is flagged as out-of-date" }) # ======================================================================== # ANALYZE LAST MODIFICATION TIME # ======================================================================== if last_modified: try: # Handle both timestamp formats if isinstance(last_modified, str): # Try to parse from formatted string last_mod_date = datetime.strptime(last_modified.split()[0], "%Y-%m-%d") else: # It's a Unix timestamp last_mod_date = datetime.fromtimestamp(last_modified) days_since_update = (datetime.now() - last_mod_date).days if days_since_update > 730: # 2 years risk_factors.append({ "category": "maintenance", "severity": "HIGH", "issue": f"Not updated in {days_since_update} days (~{days_since_update//365} years) - possibly abandoned" }) elif days_since_update > 365: # 1 year risk_factors.append({ "category": "maintenance", "severity": "MEDIUM", "issue": f"Not updated in {days_since_update} days (~{days_since_update//365} year) - low activity" }) elif days_since_update <= 30: trust_indicators.append({ "category": "maintenance", "indicator": f"Recently updated ({days_since_update} days ago) - actively maintained" }) except Exception as e: logger.debug(f"Failed to parse last_modified: {e}") # ======================================================================== # ANALYZE PACKAGE AGE # ======================================================================== if first_submitted: try: # Handle both timestamp formats if isinstance(first_submitted, str): first_submit_date = datetime.strptime(first_submitted.split()[0], "%Y-%m-%d") else: first_submit_date = datetime.fromtimestamp(first_submitted) package_age_days = (datetime.now() - first_submit_date).days if package_age_days < 7: risk_factors.append({ "category": "age", "severity": "HIGH", "issue": f"Very new package ({package_age_days} days old) - needs community review time" }) elif package_age_days < 30: risk_factors.append({ "category": "age", "severity": "MEDIUM", "issue": f"New package ({package_age_days} days old) - limited track record" }) elif package_age_days >= 365: trust_indicators.append({ "category": "age", "indicator": f"Mature package ({package_age_days//365}+ years old) - established track record" }) except Exception as e: logger.debug(f"Failed to parse first_submitted: {e}") # ======================================================================== # CALCULATE TRUST SCORE # ======================================================================== # Start with base score of 50 trust_score = 50 # Adjust based on votes (max +30) if votes >= 100: trust_score += 30 elif votes >= 50: trust_score += 20 elif votes >= 20: trust_score += 10 elif votes >= 5: trust_score += 5 elif votes == 0: trust_score -= 20 # Adjust based on popularity (max +10) if popularity >= 5.0: trust_score += 10 elif popularity >= 1.0: trust_score += 5 elif popularity < 0.001: trust_score -= 10 # Penalties for risk factors for risk in risk_factors: if risk["severity"] == "CRITICAL": trust_score -= 30 elif risk["severity"] == "HIGH": trust_score -= 15 elif risk["severity"] == "MEDIUM": trust_score -= 10 # Clamp between 0 and 100 trust_score = max(0, min(100, trust_score)) # ======================================================================== # GENERATE RECOMMENDATION # ======================================================================== if trust_score >= 70: recommendation = "✅ TRUSTED - Package has good community validation and maintenance" elif trust_score >= 50: recommendation = "⚠️ MODERATE TRUST - Package is acceptable but verify PKGBUILD carefully" elif trust_score >= 30: recommendation = "⚠️ LOW TRUST - Package has significant risk factors, extra caution needed" else: recommendation = "❌ UNTRUSTED - Package has critical trust issues, avoid unless necessary" logger.info(f"Package metadata analysis: trust_score={trust_score}, " f"{len(risk_factors)} risk factors, {len(trust_indicators)} trust indicators") return { "trust_score": trust_score, "risk_factors": risk_factors, "trust_indicators": trust_indicators, "recommendation": recommendation, "summary": { "votes": votes, "popularity": round(popularity, 4), "is_orphaned": not maintainer or maintainer == "None", "is_out_of_date": bool(out_of_date), "total_risk_factors": len(risk_factors), "total_trust_indicators": len(trust_indicators) } } - src/arch_ops_server/aur.py:919-1189 (helper)analyze_pkgbuild_safety() helper: scans PKGBUILD for dangerous commands, obfuscation, network activity, binary downloads and other security threats. Returns safe (bool), red_flags, warnings, risk_score.
def analyze_pkgbuild_safety(pkgbuild_content: str) -> Dict[str, Any]: """ Perform comprehensive safety analysis on PKGBUILD content. Checks for: - Dangerous commands (rm -rf /, dd, fork bombs, etc.) - Obfuscated code (base64, eval, encoding tricks) - Network activity (reverse shells, data exfiltration) - Binary downloads and execution - Privilege escalation attempts - Cryptocurrency mining patterns - Source URL validation - Suspicious file operations Args: pkgbuild_content: Raw PKGBUILD text Returns: Dict with detailed safety analysis results including: - safe: boolean - red_flags: critical security issues - warnings: suspicious patterns - info: informational notices - risk_score: 0-100 (higher = more dangerous) - recommendation: action recommendation """ import re from urllib.parse import urlparse red_flags = [] # Critical security issues warnings = [] # Suspicious but not necessarily malicious info = [] # Informational notices lines = pkgbuild_content.split('\n') logger.debug(f"Analyzing PKGBUILD with {len(lines)} lines") # ======================================================================== # CRITICAL PATTERNS - Definitely malicious # ======================================================================== dangerous_patterns = [ # Destructive commands (r"rm\s+-rf\s+/[^a-zA-Z]", "CRITICAL: rm -rf / or /something detected - system destruction"), (r"\bdd\b.*if=/dev/(zero|random|urandom).*of=/dev/sd", "CRITICAL: dd overwriting disk detected"), (r":\(\)\{.*:\|:.*\}", "CRITICAL: Fork bomb detected"), (r"\bmkfs\.", "CRITICAL: Filesystem formatting detected"), (r"fdisk.*-w", "CRITICAL: Partition table modification detected"), # Reverse shells and backdoors (r"/dev/tcp/\d+\.\d+\.\d+\.\d+/\d+", "CRITICAL: Reverse shell via /dev/tcp detected"), (r"nc\s+-[^-]*e\s+/bin/(ba)?sh", "CRITICAL: Netcat reverse shell detected"), (r"bash\s+-i\s+>&\s+/dev/tcp/", "CRITICAL: Interactive reverse shell detected"), (r"python.*socket.*connect", "CRITICAL: Python socket connection (potential backdoor)"), (r"perl.*socket.*connect", "CRITICAL: Perl socket connection (potential backdoor)"), # Malicious downloads and execution (r"curl[^|]*\|\s*(ba)?sh", "CRITICAL: Piping curl to shell (remote code execution)"), (r"wget[^|]*\|\s*(ba)?sh", "CRITICAL: Piping wget to shell (remote code execution)"), (r"curl.*-o.*&&.*chmod\s+\+x.*&&\s*\./", "CRITICAL: Download, make executable, and run pattern"), # Crypto mining patterns (r"xmrig|minerd|cpuminer|ccminer", "CRITICAL: Cryptocurrency miner detected"), (r"stratum\+tcp://", "CRITICAL: Mining pool connection detected"), (r"--donate-level", "CRITICAL: XMRig miner option detected"), # Rootkit/malware installation (r"chattr\s+\+i", "CRITICAL: Making files immutable (rootkit technique)"), (r"/etc/ld\.so\.preload", "CRITICAL: LD_PRELOAD manipulation (rootkit technique)"), (r"HISTFILE=/dev/null", "CRITICAL: History clearing (covering tracks)"), ] # ======================================================================== # SUSPICIOUS PATTERNS - Require careful review # ======================================================================== suspicious_patterns = [ # Obfuscation techniques (r"base64\s+-d", "Obfuscation: base64 decoding detected"), (r"xxd\s+-r", "Obfuscation: hex decoding detected"), (r"\beval\b", "Obfuscation: eval usage (can execute arbitrary code)"), (r"\$\(.*base64.*\)", "Obfuscation: base64 in command substitution"), (r"openssl\s+enc\s+-d", "Obfuscation: encrypted content decoding"), (r"echo.*\|.*sh", "Obfuscation: piping echo to shell"), (r"printf.*\|.*sh", "Obfuscation: piping printf to shell"), # Suspicious permissions and ownership (r"chmod\s+[0-7]*7[0-7]*7", "Dangerous: world-writable permissions"), (r"chown\s+root", "Suspicious: changing ownership to root"), (r"chmod\s+[u+]*s", "Suspicious: setuid/setgid (privilege escalation risk)"), # Suspicious file operations (r"mktemp.*&&.*chmod", "Suspicious: temp file creation with permission change"), (r">/dev/null\s+2>&1", "Suspicious: suppressing all output (hiding activity)"), (r"nohup.*&", "Suspicious: background process that persists"), # Network activity (r"curl.*-s.*-o", "Network: silent download detected"), (r"wget.*-q.*-O", "Network: quiet download detected"), (r"nc\s+-l", "Network: netcat listening mode (potential backdoor)"), (r"socat", "Network: socat usage (advanced networking tool)"), (r"ssh.*-R\s+\d+:", "Network: SSH reverse tunnel detected"), # Data exfiltration (r"curl.*-X\s+POST.*--data", "Data exfiltration: HTTP POST with data"), (r"tar.*\|.*ssh", "Data exfiltration: tar over SSH"), (r"scp.*-r.*\*", "Data exfiltration: recursive SCP"), # Systemd/init manipulation (r"systemctl.*enable.*\.service", "System: enabling systemd service"), (r"/etc/systemd/system/", "System: systemd unit file modification"), (r"update-rc\.d", "System: SysV init modification"), (r"@reboot", "System: cron job at reboot"), # Kernel module manipulation (r"modprobe", "System: kernel module loading"), (r"insmod", "System: kernel module insertion"), (r"/lib/modules/", "System: kernel module directory access"), # Compiler/build chain manipulation (r"gcc.*-fPIC.*-shared", "Build: creating shared library (could be malicious)"), (r"LD_PRELOAD=", "Build: LD_PRELOAD manipulation (function hijacking)"), ] # ======================================================================== # INFORMATIONAL PATTERNS - Good to know but not necessarily bad # ======================================================================== info_patterns = [ (r"sudo\s+", "Info: sudo usage detected"), (r"git\s+clone", "Info: git clone detected"), (r"make\s+install", "Info: make install detected"), (r"pip\s+install", "Info: pip install detected"), (r"npm\s+install", "Info: npm install detected"), (r"cargo\s+install", "Info: cargo install detected"), ] # ======================================================================== # SCAN PATTERNS LINE BY LINE # ======================================================================== for i, line in enumerate(lines, 1): # Skip comments and empty lines for pattern matching stripped_line = line.strip() if stripped_line.startswith('#') or not stripped_line: continue # Check dangerous patterns (red flags) for pattern, message in dangerous_patterns: if re.search(pattern, line, re.IGNORECASE): logger.warning(f"Red flag found at line {i}: {message}") red_flags.append({ "line": i, "content": line.strip()[:100], # Limit length for output "issue": message, "severity": "CRITICAL" }) # Check suspicious patterns for pattern, message in suspicious_patterns: if re.search(pattern, line, re.IGNORECASE): logger.info(f"Warning found at line {i}: {message}") warnings.append({ "line": i, "content": line.strip()[:100], "issue": message, "severity": "WARNING" }) # Check informational patterns for pattern, message in info_patterns: if re.search(pattern, line, re.IGNORECASE): info.append({ "line": i, "content": line.strip()[:100], "issue": message, "severity": "INFO" }) # ======================================================================== # ANALYZE SOURCE URLs # ======================================================================== source_urls = re.findall(r'source=\([^)]+\)|source_\w+=\([^)]+\)', pkgbuild_content, re.MULTILINE) suspicious_domains = [] # Known suspicious TLDs and patterns suspicious_tlds = ['.tk', '.ml', '.ga', '.cf', '.gq', '.cn', '.ru'] suspicious_url_patterns = [ (r'bit\.ly|tinyurl|shorturl', "URL shortener (hides true destination)"), (r'pastebin|hastebin|paste\.ee', "Paste site (common for malware hosting)"), (r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', "Raw IP address (suspicious)"), ] for source_block in source_urls: # Extract URLs from source array urls = re.findall(r'https?://[^\s\'"]+', source_block) for url in urls: try: parsed = urlparse(url) domain = parsed.netloc.lower() # Check for suspicious TLDs if any(domain.endswith(tld) for tld in suspicious_tlds): warnings.append({ "line": 0, "content": url, "issue": f"Suspicious domain TLD: {domain}", "severity": "WARNING" }) suspicious_domains.append(domain) # Check for suspicious URL patterns for pattern, message in suspicious_url_patterns: if re.search(pattern, url, re.IGNORECASE): warnings.append({ "line": 0, "content": url, "issue": message, "severity": "WARNING" }) except Exception as e: logger.debug(f"Failed to parse URL {url}: {e}") # ======================================================================== # DETECT BINARY DOWNLOADS # ======================================================================== binary_extensions = ['.bin', '.exe', '.AppImage', '.deb', '.rpm', '.jar', '.apk'] for ext in binary_extensions: if ext in pkgbuild_content.lower(): warnings.append({ "line": 0, "content": "", "issue": f"Binary file type detected: {ext}", "severity": "WARNING" }) # ======================================================================== # CALCULATE RISK SCORE # ======================================================================== # Risk scoring: red_flags = 50 points each, warnings = 5 points each, cap at 100 risk_score = min(100, (len(red_flags) * 50) + (len(warnings) * 5)) # ======================================================================== # GENERATE RECOMMENDATION # ======================================================================== if len(red_flags) > 0: recommendation = "❌ DANGEROUS - Critical security issues detected. DO NOT INSTALL." safe = False elif len(warnings) >= 5: recommendation = "⚠️ HIGH RISK - Multiple suspicious patterns detected. Review carefully before installing." safe = False elif len(warnings) > 0: recommendation = "⚠️ CAUTION - Some suspicious patterns detected. Manual review recommended." safe = True # Technically safe but needs review else: recommendation = "✅ SAFE - No critical issues detected. Standard review still recommended." safe = True logger.info(f"PKGBUILD analysis complete: {len(red_flags)} red flags, {len(warnings)} warnings, risk score: {risk_score}") return { "safe": safe, "red_flags": red_flags, "warnings": warnings, "info": info, "risk_score": risk_score, "suspicious_domains": list(set(suspicious_domains)), "recommendation": recommendation, "summary": { "total_red_flags": len(red_flags), "total_warnings": len(warnings), "total_info": len(info), "lines_analyzed": len(lines) } } - src/arch_ops_server/aur.py:153-175 (helper)add_aur_warning() utility that wraps results with prominent safety warning about AUR packages being user-produced and potentially unsafe.
return create_error_response( "NotFound", f"AUR package '{package_name}' not found" ) package_info = _format_package_info(results[0], detailed=True) logger.info(f"Successfully fetched info for {package_name}") # Wrap with safety warning return add_aur_warning(package_info) except httpx.TimeoutException: logger.error(f"AUR info fetch timed out for: {package_name}") return create_error_response( "TimeoutError", f"AUR info fetch timed out for package: {package_name}" ) except httpx.HTTPStatusError as e: logger.error(f"AUR info HTTP error: {e}") return create_error_response( "HTTPError", f"AUR info fetch failed with status {e.response.status_code}",