From 7b09f8079b06e552087685b4373a2abd0f074d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=91=80Chloe?= Date: Mon, 6 Apr 2026 20:53:23 +0800 Subject: [PATCH] feat: Add Linux security scanner tool (linux-scan.py) - Create modular security audit tool with plugin architecture - Implement Port Check: identify listening ports and processes - Implement Permission Check: find files with 777/666 permissions - Add report generation with text and JSON output formats - Include base class for easy extension with new security checks - Add comprehensive documentation and usage examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/linux-scan/README.md | 158 ++++++++++++++++++ samples/linux-scan/linux-scan.py | 122 ++++++++++++++ samples/linux-scan/report/__init__.py | 6 + samples/linux-scan/report/formatter.py | 51 ++++++ samples/linux-scan/report/reporter.py | 88 ++++++++++ .../linux-scan/security_checks/__init__.py | 7 + .../linux-scan/security_checks/base_check.py | 57 +++++++ .../security_checks/permission_check.py | 106 ++++++++++++ .../linux-scan/security_checks/port_check.py | 91 ++++++++++ samples/linux-scan/tests/__init__.py | 3 + samples/linux-scan/utils/__init__.py | 3 + 11 files changed, 692 insertions(+) create mode 100644 samples/linux-scan/README.md create mode 100644 samples/linux-scan/linux-scan.py create mode 100644 samples/linux-scan/report/__init__.py create mode 100644 samples/linux-scan/report/formatter.py create mode 100644 samples/linux-scan/report/reporter.py create mode 100644 samples/linux-scan/security_checks/__init__.py create mode 100644 samples/linux-scan/security_checks/base_check.py create mode 100644 samples/linux-scan/security_checks/permission_check.py create mode 100644 samples/linux-scan/security_checks/port_check.py create mode 100644 samples/linux-scan/tests/__init__.py create mode 100644 samples/linux-scan/utils/__init__.py diff --git a/samples/linux-scan/README.md b/samples/linux-scan/README.md new file mode 100644 index 00000000..c0b778a2 --- /dev/null +++ b/samples/linux-scan/README.md @@ -0,0 +1,158 @@ +# Linux Security Checker (linux-scan.py) + +A modular Python security audit tool for Linux systems. Performs multiple security checks and generates detailed reports. + +## Features + +- **Port Check**: Identify listening ports and their associated processes +- **Permission Check**: Find files with dangerous permissions (777, 666) +- **Modular Architecture**: Easy to add new security checks +- **Multiple Output Formats**: Text and JSON reports +- **Clear Reporting**: Severity levels and detailed findings + +## Installation + +No external dependencies required - uses Python standard library only. + +```bash +cd samples/linux-scan +``` + +## Usage + +### List Available Checks + +```bash +python3 linux-scan.py list +``` + +### Run Specific Check + +```bash +python3 linux-scan.py run port +python3 linux-scan.py run permission +``` + +### Run Multiple Checks + +```bash +python3 linux-scan.py run port,permission +``` + +### Run All Checks + +```bash +python3 linux-scan.py run all +``` + +### Output Formats + +**Text Report (default)**: +```bash +python3 linux-scan.py run all +``` + +**JSON Report**: +```bash +python3 linux-scan.py run all --format json +``` + +## Project Structure + +``` +linux-scan/ +├── linux-scan.py # Main CLI entry point +├── security_checks/ # Security check modules +│ ├── base_check.py # Abstract base class +│ ├── port_check.py # Port listening check +│ └── permission_check.py # File permission check +├── report/ # Report generation +│ ├── reporter.py # Report generation engine +│ └── formatter.py # Output formatting +├── utils/ # Utility functions +└── tests/ # Test suite +``` + +## Adding New Security Checks + +1. Create a new file in `security_checks/` (e.g., `ssh_check.py`) +2. Inherit from `BaseCheck` class +3. Implement `get_name()`, `get_description()`, and `run()` methods +4. Register in `linux-scan.py` in the `check_classes` dictionary + +**Example:** + +```python +from .base_check import BaseCheck + +class SSHCheck(BaseCheck): + def get_name(self): + return "SSH Configuration Check" + + def get_description(self): + return "Verify secure SSH configuration" + + def run(self): + # Implement your check logic + # Use self.add_result() to report findings + pass +``` + +## Severity Levels + +- **Critical** 🔴: Immediate action required +- **High** 🟠: Should be addressed soon +- **Medium** 🟡: Review and plan remediation +- **Low** 🔵: Minor issues to consider +- **Info** ⚪: Informational findings + +## Output Example + +``` +====================================================================== +LINUX SECURITY CHECK REPORT +====================================================================== + +Port Check +---------------------------------------------------------------------- + +⚪ [Info] Listening Ports Detected + Found 5 listening port(s) + 22 127.0.0.1:22 sshd + 80 0.0.0.0:80 nginx + 443 0.0.0.0:443 nginx + 3306 127.0.0.1:3306 mysqld + 5432 127.0.0.1:5432 postgres + +Permission Check +---------------------------------------------------------------------- + +✓ No issues found + +====================================================================== +SUMMARY +====================================================================== +⚪ Info : 1 + +Total findings: 1 +====================================================================== +``` + +## Notes + +- Some checks may require elevated privileges (root/sudo) for full access +- Permission checks are limited to 5 directory levels to prevent excessive scanning +- The tool gracefully handles permission denied errors during scanning + +## Future Enhancements + +- SSH configuration security audit +- Sudo rules validation +- Firewall rules analysis +- User account security review +- Package security verification +- Log file analysis + +## License + +Educational purpose - GitHub Copilot CLI for Beginners course diff --git a/samples/linux-scan/linux-scan.py b/samples/linux-scan/linux-scan.py new file mode 100644 index 00000000..8a0639f2 --- /dev/null +++ b/samples/linux-scan/linux-scan.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +Linux Security Checker - Main CLI entry point +Performs various security checks on Linux systems +""" + +import sys +import argparse +from security_checks.port_check import PortCheck +from security_checks.permission_check import PermissionCheck +from report.reporter import Reporter + + +def list_available_checks(): + """Display all available checks.""" + checks = { + 'port': 'List listening ports and associated processes', + 'permission': 'Find files with 777 or dangerous permissions', + } + + print("\nAvailable Security Checks:\n") + for name, description in checks.items(): + print(f" {name:15} - {description}") + print() + + +def run_checks(check_names, output_format='text'): + """Run specified security checks and generate report.""" + checks_to_run = [] + + # Map check names to check classes + check_classes = { + 'port': PortCheck, + 'permission': PermissionCheck, + } + + # Validate check names + for name in check_names: + if name not in check_classes: + print(f"Error: Unknown check '{name}'") + return False + + # Instantiate checks + for name in check_names: + check_class = check_classes[name] + checks_to_run.append(check_class()) + + # Run all checks + print("\nRunning security checks...\n") + results = [] + + for check in checks_to_run: + print(f"Running {check.get_name()}...") + try: + check.run() + results.append(check) + except Exception as e: + print(f" Error: {e}") + + # Generate report + reporter = Reporter(results, output_format) + reporter.generate_report() + + return True + + +def main(): + parser = argparse.ArgumentParser( + description='Linux Security Checker - Audit your system for security issues', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python linux-scan.py list # Show available checks + python linux-scan.py run port # Run single check + python linux-scan.py run all # Run all checks + python linux-scan.py run port,permission # Run multiple checks + python linux-scan.py run all --format json # Output as JSON + """ + ) + + subparsers = parser.add_subparsers(dest='command', help='Commands') + + # List command + subparsers.add_parser('list', help='List available security checks') + + # Run command + run_parser = subparsers.add_parser('run', help='Run security checks') + run_parser.add_argument( + 'checks', + help="Check name(s): 'port', 'permission', or 'all' (comma-separated)" + ) + run_parser.add_argument( + '--format', + choices=['text', 'json'], + default='text', + help='Output format (default: text)' + ) + + # Parse arguments + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + # Handle list command + if args.command == 'list': + list_available_checks() + + # Handle run command + elif args.command == 'run': + if args.checks.lower() == 'all': + check_names = ['port', 'permission'] + else: + check_names = [c.strip() for c in args.checks.split(',')] + + success = run_checks(check_names, args.format) + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + main() diff --git a/samples/linux-scan/report/__init__.py b/samples/linux-scan/report/__init__.py new file mode 100644 index 00000000..8fce1802 --- /dev/null +++ b/samples/linux-scan/report/__init__.py @@ -0,0 +1,6 @@ +"""Package initialization for report generation.""" + +from .reporter import Reporter +from .formatter import Formatter + +__all__ = ['Reporter', 'Formatter'] diff --git a/samples/linux-scan/report/formatter.py b/samples/linux-scan/report/formatter.py new file mode 100644 index 00000000..5448f36f --- /dev/null +++ b/samples/linux-scan/report/formatter.py @@ -0,0 +1,51 @@ +"""Formatting utilities for report output.""" + + +class Formatter: + """Format security check results for display.""" + + SEVERITY_ICONS = { + 'Critical': '🔴', + 'High': '🟠', + 'Medium': '🟡', + 'Low': '🔵', + 'Info': '⚪' + } + + SEVERITY_COLORS = { + 'Critical': '\033[91m', # Red + 'High': '\033[93m', # Yellow + 'Medium': '\033[94m', # Blue + 'Low': '\033[92m', # Green + 'Info': '\033[96m' # Cyan + } + + RESET_COLOR = '\033[0m' + + def format_finding(self, result): + """Format a single finding for display.""" + severity = result['severity'] + title = result['title'] + description = result['description'] + details = result.get('details', []) + + # Build output + icon = self.get_severity_icon(severity) + + output = f"\n{icon} [{severity}] {title}\n" + output += f" {description}\n" + + if details: + for detail in details: + output += f" {detail}\n" + + return output + + def get_severity_icon(self, severity): + """Get the icon for a severity level.""" + return self.SEVERITY_ICONS.get(severity, '❓') + + def colorize(self, text, severity): + """Add color to text based on severity.""" + color = self.SEVERITY_COLORS.get(severity, '') + return f"{color}{text}{self.RESET_COLOR}" diff --git a/samples/linux-scan/report/reporter.py b/samples/linux-scan/report/reporter.py new file mode 100644 index 00000000..21a2dd00 --- /dev/null +++ b/samples/linux-scan/report/reporter.py @@ -0,0 +1,88 @@ +"""Report generation for security check results.""" + +import json +from .formatter import Formatter + + +class Reporter: + """Generate reports from security check results.""" + + def __init__(self, checks, output_format='text'): + self.checks = checks + self.output_format = output_format + self.formatter = Formatter() + + def generate_report(self): + """Generate and output the report.""" + if self.output_format == 'json': + self._generate_json_report() + else: + self._generate_text_report() + + def _generate_text_report(self): + """Generate human-readable text report.""" + print("\n" + "=" * 70) + print("LINUX SECURITY CHECK REPORT") + print("=" * 70) + + total_findings = 0 + severity_count = { + 'Critical': 0, + 'High': 0, + 'Medium': 0, + 'Low': 0, + 'Info': 0 + } + + # Process each check + for check in self.checks: + results = check.get_results() + + if results: + print(f"\n{check.get_name()}") + print("-" * 70) + + for result in results: + total_findings += 1 + severity = result['severity'] + severity_count[severity] += 1 + + # Format the finding + output = self.formatter.format_finding(result) + print(output) + else: + print(f"\n{check.get_name()}") + print("-" * 70) + print(" ✓ No issues found") + + # Summary + print("\n" + "=" * 70) + print("SUMMARY") + print("=" * 70) + + for severity in ['Critical', 'High', 'Medium', 'Low', 'Info']: + count = severity_count[severity] + if count > 0: + icon = self.formatter.get_severity_icon(severity) + print(f"{icon} {severity:10}: {count}") + + print(f"\nTotal findings: {total_findings}") + print("=" * 70 + "\n") + + def _generate_json_report(self): + """Generate JSON format report.""" + report = { + 'report_type': 'Linux Security Check', + 'checks': [] + } + + for check in self.checks: + check_report = { + 'name': check.get_name(), + 'description': check.get_description(), + 'results': check.get_results(), + 'summary': check.get_summary() + } + report['checks'].append(check_report) + + print(json.dumps(report, indent=2)) diff --git a/samples/linux-scan/security_checks/__init__.py b/samples/linux-scan/security_checks/__init__.py new file mode 100644 index 00000000..48c218f1 --- /dev/null +++ b/samples/linux-scan/security_checks/__init__.py @@ -0,0 +1,7 @@ +"""Package initialization for security checks.""" + +from .base_check import BaseCheck +from .port_check import PortCheck +from .permission_check import PermissionCheck + +__all__ = ['BaseCheck', 'PortCheck', 'PermissionCheck'] diff --git a/samples/linux-scan/security_checks/base_check.py b/samples/linux-scan/security_checks/base_check.py new file mode 100644 index 00000000..cbd5f135 --- /dev/null +++ b/samples/linux-scan/security_checks/base_check.py @@ -0,0 +1,57 @@ +"""Base class for all security checks.""" + +from abc import ABC, abstractmethod + + +class BaseCheck(ABC): + """Abstract base class for security checks.""" + + def __init__(self): + self.results = [] + self.executed = False + + @abstractmethod + def get_name(self): + """Return the name of this check.""" + pass + + @abstractmethod + def get_description(self): + """Return a description of this check.""" + pass + + @abstractmethod + def run(self): + """Execute the security check. Should populate self.results.""" + pass + + def get_results(self): + """Return the results of the check.""" + return self.results + + def add_result(self, severity, title, description, details=None): + """Add a finding to the results.""" + result = { + 'severity': severity, # 'Critical', 'High', 'Medium', 'Low', 'Info' + 'title': title, + 'description': description, + 'details': details or [] + } + self.results.append(result) + + def has_findings(self): + """Check if this check found any issues.""" + return len(self.results) > 0 + + def get_summary(self): + """Get a summary of findings by severity.""" + summary = { + 'Critical': 0, + 'High': 0, + 'Medium': 0, + 'Low': 0, + 'Info': 0 + } + for result in self.results: + summary[result['severity']] += 1 + return summary diff --git a/samples/linux-scan/security_checks/permission_check.py b/samples/linux-scan/security_checks/permission_check.py new file mode 100644 index 00000000..061236ae --- /dev/null +++ b/samples/linux-scan/security_checks/permission_check.py @@ -0,0 +1,106 @@ +"""Check for files with dangerous permissions (777, 666, etc.).""" + +import os +import stat +from pathlib import Path +from .base_check import BaseCheck + + +class PermissionCheck(BaseCheck): + """Find files with overly permissive permissions.""" + + # Common system paths to check + PATHS_TO_SCAN = [ + '/etc', + '/home', + '/root', + '/tmp', + '/var', + ] + + # Maximum depth to prevent excessive scanning + MAX_DEPTH = 5 + + def get_name(self): + return "Permission Check" + + def get_description(self): + return "Find files with 777 or dangerous permissions" + + def run(self): + """Execute the permission check.""" + self.results = [] + dangerous_files = [] + + for scan_path in self.PATHS_TO_SCAN: + if not os.path.exists(scan_path): + continue + + try: + self._scan_directory(scan_path, dangerous_files) + except PermissionError: + # Skip directories we don't have permission to read + pass + except Exception as e: + self.add_result( + 'Low', + f'Error scanning {scan_path}', + f'Could not fully scan {scan_path}: {str(e)}' + ) + + # Report findings + if dangerous_files: + self.add_result( + 'High', + 'Dangerous File Permissions Detected', + f'Found {len(dangerous_files)} file(s) with overly permissive permissions', + dangerous_files[:50] # Limit output to first 50 + ) + + def _scan_directory(self, path, dangerous_files, depth=0): + """Recursively scan directory for dangerous permissions.""" + if depth > self.MAX_DEPTH: + return + + try: + for entry in os.scandir(path): + try: + # Get file stats + st = entry.stat(follow_symlinks=False) + mode = st.st_mode + + # Check if file has dangerous permissions + if self._is_dangerous_permission(mode): + perm_str = oct(stat.S_IMODE(mode)) + dangerous_files.append( + f" {perm_str} {entry.path}" + ) + + # Recursively scan subdirectories + if entry.is_dir(follow_symlinks=False): + self._scan_directory(entry.path, dangerous_files, depth + 1) + + except (PermissionError, OSError): + # Skip files we can't access + pass + + except PermissionError: + pass + + @staticmethod + def _is_dangerous_permission(mode): + """Check if file permissions are dangerous.""" + # Extract the permission bits (last 9 bits) + perm = stat.S_IMODE(mode) + + # Check for dangerous permissions: + # 0o777 - read, write, execute for all + # 0o666 - read, write for all (files) + # 0o644 - world-readable, group-readable (for sensitive files) + + dangerous = [ + 0o777, # rwxrwxrwx + 0o666, # rw-rw-rw- + ] + + return perm in dangerous diff --git a/samples/linux-scan/security_checks/port_check.py b/samples/linux-scan/security_checks/port_check.py new file mode 100644 index 00000000..b6c98d62 --- /dev/null +++ b/samples/linux-scan/security_checks/port_check.py @@ -0,0 +1,91 @@ +"""Check for listening ports and associated processes.""" + +import subprocess +from .base_check import BaseCheck + + +class PortCheck(BaseCheck): + """Identify listening ports and the processes that opened them.""" + + def get_name(self): + return "Port Check" + + def get_description(self): + return "List listening ports and associated processes" + + def run(self): + """Execute the port check.""" + self.results = [] + + try: + # Use ss (socket statistics) to get listening ports + # Format: -tlnp (tcp, listening, numeric, process) + result = subprocess.run( + ['ss', '-tlnp'], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode != 0: + self.add_result( + 'High', + 'Unable to list ports', + 'Failed to execute ss command', + [result.stderr] + ) + return + + # Parse the output + lines = result.stdout.strip().split('\n') + listening_ports = [] + + # Skip header line (LISTEN Local Address ...) + for line in lines[1:]: + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 4: + # Extract address and process info + local_addr = parts[3] + process_info = parts[-1] if len(parts) > 4 else "unknown" + + # Extract port from address (format: [::]:PORT or IP:PORT) + port = local_addr.split(':')[-1] + + listening_ports.append({ + 'port': port, + 'address': local_addr, + 'process': process_info + }) + + # Add result as Info (not a problem, just informational) + if listening_ports: + self.add_result( + 'Info', + 'Listening Ports Detected', + f'Found {len(listening_ports)} listening port(s)', + [f" {p['port']:<6} {p['address']:<30} {p['process']}" + for p in listening_ports] + ) + + except FileNotFoundError: + self.add_result( + 'High', + 'ss command not found', + 'The ss utility is not available on this system', + ['Please install net-tools or iproute2 package'] + ) + except subprocess.TimeoutExpired: + self.add_result( + 'High', + 'Port check timeout', + 'The port check timed out after 10 seconds' + ) + except Exception as e: + self.add_result( + 'High', + 'Port check error', + f'An unexpected error occurred: {str(e)}' + ) diff --git a/samples/linux-scan/tests/__init__.py b/samples/linux-scan/tests/__init__.py new file mode 100644 index 00000000..d2916893 --- /dev/null +++ b/samples/linux-scan/tests/__init__.py @@ -0,0 +1,3 @@ +"""Package initialization for tests.""" + +__all__ = [] diff --git a/samples/linux-scan/utils/__init__.py b/samples/linux-scan/utils/__init__.py new file mode 100644 index 00000000..f98111d0 --- /dev/null +++ b/samples/linux-scan/utils/__init__.py @@ -0,0 +1,3 @@ +"""Package initialization for utilities.""" + +__all__ = []