Remove SVG diagram functionality
- Delete svg_generator.py and comprehensive_mapper.py - Remove --generate-svg option from integrated_scanner.py - Update complete_workflow.sh to remove SVG generation step - Clean up documentation and examples - Update test_system.py to remove SVG references - Add missing files to repository (EXAMPLES.sh, quickstart.sh, etc.)
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,9 +1,11 @@
|
|||||||
# Network Scanner - Git Ignore
|
# Network Scanner - Git Ignore
|
||||||
|
|
||||||
# Scan results and output
|
# Scan results and output
|
||||||
*.json
|
# Ignore archived results in results/ folder
|
||||||
|
results/
|
||||||
|
|
||||||
|
# Keep config.json.example
|
||||||
!config.json.example
|
!config.json.example
|
||||||
*.svg
|
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
260
EXAMPLES.sh
Normal file
260
EXAMPLES.sh
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Example usage scenarios for the network scanner
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Network Scanner - Usage Examples"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cat << 'EOF'
|
||||||
|
# SCENARIO 1: Quick Network Overview
|
||||||
|
# -----------------------------------
|
||||||
|
# Scan your local network and get a basic overview
|
||||||
|
|
||||||
|
./network_scanner.py -v -o quick_scan.json
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 2: Complete Network Documentation
|
||||||
|
# -------------------------------------------
|
||||||
|
# Full scan with pfSense integration
|
||||||
|
|
||||||
|
./integrated_scanner.py -c config.json -o full_network.json -v
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 3: pfSense Deep Dive
|
||||||
|
# ------------------------------
|
||||||
|
# Detailed scan of a specific pfSense firewall
|
||||||
|
|
||||||
|
./pfsense_scanner.py 192.168.1.1 -u root -k ~/.ssh/id_rsa -o pfsense_main.json
|
||||||
|
|
||||||
|
# View the results:
|
||||||
|
cat pfsense_main.json | jq '.vpn' # Show VPN info
|
||||||
|
cat pfsense_main.json | jq '.routes' # Show routing table
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 4: Multi-Network Scan with VPN
|
||||||
|
# ----------------------------------------
|
||||||
|
# Create a config for multiple networks
|
||||||
|
|
||||||
|
cat > my_network_config.json << 'CONFIG'
|
||||||
|
{
|
||||||
|
"ssh_user": "root",
|
||||||
|
"ssh_key_path": "/home/user/.ssh/id_rsa",
|
||||||
|
"timeout": 3,
|
||||||
|
"additional_networks": [
|
||||||
|
"192.168.1.0/24", # Main network
|
||||||
|
"192.168.2.0/24", # Guest network
|
||||||
|
"10.8.0.0/24", # OpenVPN network
|
||||||
|
"10.0.0.0/24" # WireGuard VPN
|
||||||
|
],
|
||||||
|
"special_devices": {
|
||||||
|
"192.168.1.1": {
|
||||||
|
"name": "Main pfSense Firewall",
|
||||||
|
"type": "firewall",
|
||||||
|
"os": "pfSense"
|
||||||
|
},
|
||||||
|
"192.168.2.1": {
|
||||||
|
"name": "Guest Network Router",
|
||||||
|
"type": "router"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONFIG
|
||||||
|
|
||||||
|
./integrated_scanner.py -c my_network_config.json -o multi_network.json
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 5: Scheduled Network Monitoring
|
||||||
|
# -----------------------------------------
|
||||||
|
# Add to crontab for daily network documentation
|
||||||
|
|
||||||
|
# Create wrapper script
|
||||||
|
cat > /usr/local/bin/network-scan-daily.sh << 'SCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
DATE=$(date +%Y%m%d)
|
||||||
|
OUTPUT_DIR="/var/log/network-scans"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
cd /path/to/network_scanner
|
||||||
|
./integrated_scanner.py \
|
||||||
|
-o "$OUTPUT_DIR/scan_$DATE.json"
|
||||||
|
|
||||||
|
# Keep only last 30 days
|
||||||
|
find "$OUTPUT_DIR" -name "scan_*.json" -mtime +30 -delete
|
||||||
|
SCRIPT
|
||||||
|
|
||||||
|
chmod +x /usr/local/bin/network-scan-daily.sh
|
||||||
|
|
||||||
|
# Add to crontab (run at 2 AM daily):
|
||||||
|
# 0 2 * * * /usr/local/bin/network-scan-daily.sh
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 6: Compare Network Changes
|
||||||
|
# ------------------------------------
|
||||||
|
# Scan and compare with previous results
|
||||||
|
|
||||||
|
# Initial scan
|
||||||
|
./integrated_scanner.py -o baseline.json
|
||||||
|
|
||||||
|
# After changes
|
||||||
|
./integrated_scanner.py -o current.json
|
||||||
|
|
||||||
|
# Compare device counts
|
||||||
|
echo "Baseline devices:"
|
||||||
|
cat baseline.json | jq '[.segments[].devices[].ip] | length'
|
||||||
|
echo "Current devices:"
|
||||||
|
cat current.json | jq '[.segments[].devices[].ip] | length'
|
||||||
|
|
||||||
|
# Find new devices
|
||||||
|
comm -13 \
|
||||||
|
<(cat baseline.json | jq -r '.segments[].devices[].ip' | sort) \
|
||||||
|
<(cat current.json | jq -r '.segments[].devices[].ip' | sort) \
|
||||||
|
| sed 's/^/NEW: /'
|
||||||
|
|
||||||
|
# Find removed devices
|
||||||
|
comm -23 \
|
||||||
|
<(cat baseline.json | jq -r '.segments[].devices[].ip' | sort) \
|
||||||
|
<(cat current.json | jq -r '.segments[].devices[].ip' | sort) \
|
||||||
|
| sed 's/^/REMOVED: /'
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 7: Extract Specific Information
|
||||||
|
# -----------------------------------------
|
||||||
|
# Use jq to extract specific data from scan results
|
||||||
|
|
||||||
|
# List all SSH-accessible devices
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | select(.ssh_accessible==true) | .ip'
|
||||||
|
|
||||||
|
# List all routers/firewalls
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | select(.device_type=="router" or .device_type=="firewall") | "\(.ip) - \(.hostname // "unknown")"'
|
||||||
|
|
||||||
|
# List all devices with their OS
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | "\(.ip)\t\(.os_type // "unknown")\t\(.hostname // "unknown")"'
|
||||||
|
|
||||||
|
# Export to CSV
|
||||||
|
echo "IP,Hostname,Type,OS" > devices.csv
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | "\(.ip),\(.hostname // ""),\(.device_type // ""),\(.os_type // "")"' >> devices.csv
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 8: Integration with Documentation
|
||||||
|
# -------------------------------------------
|
||||||
|
# Generate markdown documentation from scan
|
||||||
|
|
||||||
|
cat > generate_docs.py << 'PYTHON'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
print("# Network Documentation")
|
||||||
|
print(f"\nGenerated: {data.get('scan_timestamp', 'N/A')}")
|
||||||
|
print("\n## Network Segments\n")
|
||||||
|
|
||||||
|
for segment in data['segments']:
|
||||||
|
print(f"### {segment['name']}")
|
||||||
|
print(f"- CIDR: `{segment['cidr']}`")
|
||||||
|
print(f"- Devices: {len(segment['devices'])}")
|
||||||
|
if segment.get('is_vpn'):
|
||||||
|
print("- Type: VPN Network")
|
||||||
|
print("\n#### Devices\n")
|
||||||
|
print("| IP | Hostname | Type | OS |")
|
||||||
|
print("|---|---|---|---|")
|
||||||
|
|
||||||
|
for device in segment['devices']:
|
||||||
|
ip = device['ip']
|
||||||
|
hostname = device.get('hostname', '-')
|
||||||
|
dtype = device.get('device_type', '-')
|
||||||
|
os = device.get('os_type', '-')
|
||||||
|
print(f"| {ip} | {hostname} | {dtype} | {os} |")
|
||||||
|
|
||||||
|
print()
|
||||||
|
PYTHON
|
||||||
|
|
||||||
|
chmod +x generate_docs.py
|
||||||
|
./generate_docs.py network_scan.json > NETWORK_DOCS.md
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 9: Security Audit
|
||||||
|
# ---------------------------
|
||||||
|
# Check for common security issues
|
||||||
|
|
||||||
|
# Find devices with Telnet open
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | select(.open_ports[]? == 23) | "⚠️ Telnet open on \(.ip) (\(.hostname // "unknown"))"'
|
||||||
|
|
||||||
|
# Find devices without SSH access
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | select(.device_type=="router" or .device_type=="firewall") | select(.ssh_accessible==false) | "⚠️ No SSH access to \(.ip) (\(.hostname // "unknown"))"'
|
||||||
|
|
||||||
|
# List devices with many open ports
|
||||||
|
cat network_scan.json | jq -r '.segments[].devices[] | select((.open_ports | length) > 5) | "ℹ️ \(.ip) has \(.open_ports | length) open ports"'
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 10: WireGuard Topology Mapping
|
||||||
|
# ----------------------------------------
|
||||||
|
# Extract WireGuard tunnel information from pfSense
|
||||||
|
|
||||||
|
./pfsense_scanner.py 192.168.1.1 -o pfsense.json
|
||||||
|
|
||||||
|
# List all WireGuard peers
|
||||||
|
cat pfsense.json | jq -r '.vpn.wireguard[] | "Peer: \(.peer // "N/A") -> \(.allowed_ips // "N/A")"'
|
||||||
|
|
||||||
|
# Check tunnel status
|
||||||
|
cat pfsense.json | jq -r '.vpn.wireguard[] | select(.latest_handshake) | "Active tunnel to \(.endpoint) (handshake: \(.latest_handshake)s ago)"'
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 11: Network Capacity Planning
|
||||||
|
# ---------------------------------------
|
||||||
|
# Analyze network usage and plan capacity
|
||||||
|
|
||||||
|
# Count devices per segment
|
||||||
|
cat network_scan.json | jq -r '.segments[] | "\(.cidr): \(.devices | length) devices"'
|
||||||
|
|
||||||
|
# Calculate subnet utilization
|
||||||
|
cat network_scan.json | jq -r '.segments[] |
|
||||||
|
if .cidr | contains("/24") then
|
||||||
|
"\(.cidr): \(.devices | length)/254 = \((.devices | length) * 100 / 254 | floor)% utilized"
|
||||||
|
else
|
||||||
|
"\(.cidr): \(.devices | length) devices"
|
||||||
|
end'
|
||||||
|
|
||||||
|
|
||||||
|
# SCENARIO 12: Quick Health Check
|
||||||
|
# --------------------------------
|
||||||
|
# Create a health check script
|
||||||
|
|
||||||
|
cat > health_check.sh << 'HEALTH'
|
||||||
|
#!/bin/bash
|
||||||
|
SCAN_FILE="latest_scan.json"
|
||||||
|
|
||||||
|
echo "Network Health Check"
|
||||||
|
echo "===================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Total devices
|
||||||
|
TOTAL=$(cat $SCAN_FILE | jq '[.segments[].devices[]] | length')
|
||||||
|
echo "Total devices: $TOTAL"
|
||||||
|
|
||||||
|
# SSH accessible
|
||||||
|
SSH_OK=$(cat $SCAN_FILE | jq '[.segments[].devices[] | select(.ssh_accessible==true)] | length')
|
||||||
|
echo "SSH accessible: $SSH_OK"
|
||||||
|
|
||||||
|
# By type
|
||||||
|
echo ""
|
||||||
|
echo "Device Types:"
|
||||||
|
cat $SCAN_FILE | jq -r '.segments[].devices[].device_type' | sort | uniq -c | sort -rn
|
||||||
|
|
||||||
|
# Segments
|
||||||
|
echo ""
|
||||||
|
echo "Network Segments:"
|
||||||
|
cat $SCAN_FILE | jq -r '.segments[] | " \(.name): \(.devices | length) devices"'
|
||||||
|
HEALTH
|
||||||
|
|
||||||
|
chmod +x health_check.sh
|
||||||
|
./integrated_scanner.py -o latest_scan.json
|
||||||
|
./health_check.sh
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "For more examples, see README.md"
|
||||||
@@ -32,6 +32,14 @@ log_error() {
|
|||||||
echo -e "${RED}[ERROR]${NC} $1"
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Move old results to results folder
|
||||||
|
log_info "Moving old results to results folder..."
|
||||||
|
mkdir -p results
|
||||||
|
mv network_scan_*.json server_details_*.json network_summary_*.md *_failed_ssh.json results/ 2>/dev/null || true
|
||||||
|
if [ $? -eq 0 ] && [ "$(ls results/ 2>/dev/null | wc -l)" -gt 0 ]; then
|
||||||
|
log_info "Moved old result files to results/ folder"
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if we're in the right directory
|
# Check if we're in the right directory
|
||||||
if [ ! -f "src/integrated_scanner.py" ]; then
|
if [ ! -f "src/integrated_scanner.py" ]; then
|
||||||
log_error "src/integrated_scanner.py not found. Please run this script from the network scanner directory."
|
log_error "src/integrated_scanner.py not found. Please run this script from the network scanner directory."
|
||||||
@@ -60,24 +68,30 @@ log_info "Step 2: Running integrated network scan..."
|
|||||||
SCAN_OUTPUT="network_scan_$(date +%Y%m%d_%H%M%S).json"
|
SCAN_OUTPUT="network_scan_$(date +%Y%m%d_%H%M%S).json"
|
||||||
if python3 src/integrated_scanner.py -o "$SCAN_OUTPUT" -v; then
|
if python3 src/integrated_scanner.py -o "$SCAN_OUTPUT" -v; then
|
||||||
log_success "Network scan completed: $SCAN_OUTPUT"
|
log_success "Network scan completed: $SCAN_OUTPUT"
|
||||||
|
|
||||||
|
# Check for failed SSH hosts file
|
||||||
|
FAILED_SSH_OUTPUT="${SCAN_OUTPUT%.json}_failed_ssh.json"
|
||||||
|
if [ -f "$FAILED_SSH_OUTPUT" ]; then
|
||||||
|
FAILED_COUNT=$(jq '.total_failed' "$FAILED_SSH_OUTPUT" 2>/dev/null || echo "unknown")
|
||||||
|
log_warning "Found $FAILED_COUNT hosts with SSH port open but failed authentication: $FAILED_SSH_OUTPUT"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log_error "Network scan failed"
|
log_error "Network scan failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 3: Generate SVG diagram
|
# Step 3: Collect server information from hypervisors
|
||||||
log_info "Step 3: Generating network diagram..."
|
log_info "Step 3: Collecting server information from hypervisors..."
|
||||||
SVG_OUTPUT="${SCAN_OUTPUT%.json}.svg"
|
SERVER_OUTPUT="server_details_$(date +%Y%m%d_%H%M%S).json"
|
||||||
if python3 src/svg_generator.py "$SCAN_OUTPUT" -o "$SVG_OUTPUT"; then
|
if python3 src/server_info_collector.py -o "$SERVER_OUTPUT"; then
|
||||||
log_success "SVG diagram generated: $SVG_OUTPUT"
|
log_success "Server information collected: $SERVER_OUTPUT"
|
||||||
else
|
else
|
||||||
log_error "SVG generation failed"
|
log_warning "Server information collection failed"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 4: Generate pfSense summary if XML files exist
|
# Step 5: Generate pfSense summary if XML files exist
|
||||||
if [ "$XML_FILES" -gt 0 ]; then
|
if [ "$XML_FILES" -gt 0 ]; then
|
||||||
log_info "Step 4: Generating pfSense network summary..."
|
log_info "Step 5: Generating pfSense network summary..."
|
||||||
SUMMARY_OUTPUT="network_summary_$(date +%Y%m%d_%H%M%S).md"
|
SUMMARY_OUTPUT="network_summary_$(date +%Y%m%d_%H%M%S).md"
|
||||||
if python3 src/pfsense_integrator.py *.xml --summary "$SUMMARY_OUTPUT"; then
|
if python3 src/pfsense_integrator.py *.xml --summary "$SUMMARY_OUTPUT"; then
|
||||||
log_success "Network summary generated: $SUMMARY_OUTPUT"
|
log_success "Network summary generated: $SUMMARY_OUTPUT"
|
||||||
@@ -86,7 +100,7 @@ if [ "$XML_FILES" -gt 0 ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 5: Show results summary
|
# Step 6: Show results summary
|
||||||
echo ""
|
echo ""
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
log_success "Network Discovery Complete!"
|
log_success "Network Discovery Complete!"
|
||||||
@@ -94,7 +108,12 @@ echo "=========================================="
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Generated files:"
|
echo "Generated files:"
|
||||||
echo " 📊 Network Scan: $SCAN_OUTPUT"
|
echo " 📊 Network Scan: $SCAN_OUTPUT"
|
||||||
echo " 🎨 Network Diagram: $SVG_OUTPUT"
|
if [ -f "$SERVER_OUTPUT" ]; then
|
||||||
|
echo " 🖥️ Server Details: $SERVER_OUTPUT"
|
||||||
|
fi
|
||||||
|
if [ -f "$FAILED_SSH_OUTPUT" ]; then
|
||||||
|
echo " 🔐 Failed SSH Hosts: $FAILED_SSH_OUTPUT"
|
||||||
|
fi
|
||||||
if [ "$XML_FILES" -gt 0 ]; then
|
if [ "$XML_FILES" -gt 0 ]; then
|
||||||
echo " 📋 Network Summary: $SUMMARY_OUTPUT"
|
echo " 📋 Network Summary: $SUMMARY_OUTPUT"
|
||||||
fi
|
fi
|
||||||
@@ -114,11 +133,21 @@ if command -v jq >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Open $SVG_OUTPUT in your web browser to view the network diagram"
|
if [ -f "$SERVER_OUTPUT" ]; then
|
||||||
if [ "$XML_FILES" -gt 0 ]; then
|
echo " 1. Review $SERVER_OUTPUT for detailed server and VM information"
|
||||||
echo " 2. Review $SUMMARY_OUTPUT for detailed pfSense configuration"
|
STEP_NUM=2
|
||||||
|
else
|
||||||
|
STEP_NUM=1
|
||||||
fi
|
fi
|
||||||
echo " 3. Examine $SCAN_OUTPUT for complete network data (use jq for querying)"
|
if [ -f "$FAILED_SSH_OUTPUT" ]; then
|
||||||
|
echo " $STEP_NUM. Review $FAILED_SSH_OUTPUT for hosts needing SSH credential fixes"
|
||||||
|
STEP_NUM=$((STEP_NUM + 1))
|
||||||
|
fi
|
||||||
|
if [ "$XML_FILES" -gt 0 ]; then
|
||||||
|
echo " $STEP_NUM. Review $SUMMARY_OUTPUT for detailed pfSense configuration"
|
||||||
|
STEP_NUM=$((STEP_NUM + 1))
|
||||||
|
fi
|
||||||
|
echo " $STEP_NUM. Examine $SCAN_OUTPUT for complete network data (use jq for querying)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
log_success "Workflow completed successfully! 🎉"
|
log_success "Workflow completed successfully! 🎉"
|
||||||
@@ -17,5 +17,18 @@
|
|||||||
"max_workers": 10,
|
"max_workers": 10,
|
||||||
"ping_timeout": 2,
|
"ping_timeout": 2,
|
||||||
"port_scan_timeout": 1
|
"port_scan_timeout": 1
|
||||||
}
|
},
|
||||||
|
"hypervisors": [
|
||||||
|
{
|
||||||
|
"host": "srvhost04.egonetix.de",
|
||||||
|
"port": 2222,
|
||||||
|
"user": "root"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "srv-wmw-host01",
|
||||||
|
"port": 22,
|
||||||
|
"user": "root"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"probe_ssh_on_discovered": true
|
||||||
}
|
}
|
||||||
|
|||||||
165
quickstart.sh
Normal file
165
quickstart.sh
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick Start Script for Network Scanner
|
||||||
|
# This script helps you get started quickly
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "================================"
|
||||||
|
echo "Network Scanner - Quick Start"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for Python
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
echo "❌ Error: Python 3 is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Python 3 found"
|
||||||
|
|
||||||
|
# Create config if it doesn't exist
|
||||||
|
if [ ! -f config.json ]; then
|
||||||
|
echo ""
|
||||||
|
echo "📝 Creating configuration file..."
|
||||||
|
|
||||||
|
# Try to detect default SSH key
|
||||||
|
SSH_KEY=""
|
||||||
|
if [ -f ~/.ssh/id_rsa ]; then
|
||||||
|
SSH_KEY="$HOME/.ssh/id_rsa"
|
||||||
|
elif [ -f ~/.ssh/id_ed25519 ]; then
|
||||||
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current user
|
||||||
|
CURRENT_USER=$(whoami)
|
||||||
|
|
||||||
|
# Try to detect local network
|
||||||
|
LOCAL_NET=$(ip route | grep -oP 'src \K[\d.]+' | head -1)
|
||||||
|
if [ -n "$LOCAL_NET" ]; then
|
||||||
|
# Convert to /24 network
|
||||||
|
NET_PREFIX=$(echo $LOCAL_NET | cut -d. -f1-3)
|
||||||
|
LOCAL_NET="${NET_PREFIX}.0/24"
|
||||||
|
else
|
||||||
|
LOCAL_NET="192.168.1.0/24"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > config.json << EOF
|
||||||
|
{
|
||||||
|
"ssh_user": "$CURRENT_USER",
|
||||||
|
"ssh_key_path": "$SSH_KEY",
|
||||||
|
"timeout": 2,
|
||||||
|
"additional_networks": [
|
||||||
|
"$LOCAL_NET"
|
||||||
|
],
|
||||||
|
"special_devices": {
|
||||||
|
},
|
||||||
|
"scan_options": {
|
||||||
|
"max_workers": 10,
|
||||||
|
"ping_timeout": 2,
|
||||||
|
"port_scan_timeout": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✓ Created config.json"
|
||||||
|
echo " Local network detected: $LOCAL_NET"
|
||||||
|
[ -n "$SSH_KEY" ] && echo " SSH key detected: $SSH_KEY"
|
||||||
|
echo ""
|
||||||
|
echo " Please edit config.json to customize for your network!"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "✓ config.json already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask what to do
|
||||||
|
echo ""
|
||||||
|
echo "What would you like to do?"
|
||||||
|
echo ""
|
||||||
|
echo "1) Run a quick scan (current network only)"
|
||||||
|
echo "2) Run a full scan with pfSense integration"
|
||||||
|
echo "3) Scan specific pfSense device"
|
||||||
|
echo "4) Show help"
|
||||||
|
echo "5) Exit"
|
||||||
|
echo ""
|
||||||
|
read -p "Choose an option (1-6): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Running quick network scan..."
|
||||||
|
./network_scanner.py -o quick_scan.json -v
|
||||||
|
echo ""
|
||||||
|
echo "✓ Done! Results saved to: quick_scan.json"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Running full integrated scan..."
|
||||||
|
./integrated_scanner.py -o full_scan.json -v
|
||||||
|
echo ""
|
||||||
|
echo "✓ Done! Results saved to: full_scan.json"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo ""
|
||||||
|
read -p "Enter pfSense IP address: " pfsense_ip
|
||||||
|
echo "🔍 Scanning pfSense at $pfsense_ip..."
|
||||||
|
./pfsense_scanner.py "$pfsense_ip" -o "pfsense_${pfsense_ip}.json"
|
||||||
|
echo ""
|
||||||
|
echo "✓ Done! Results saved to: pfsense_${pfsense_ip}.json"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo ""
|
||||||
|
cat << 'HELP'
|
||||||
|
Network Scanner - Help
|
||||||
|
======================
|
||||||
|
|
||||||
|
Available Scripts:
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
1. network_scanner.py
|
||||||
|
Basic network scanner that discovers devices and gathers info
|
||||||
|
Usage: ./network_scanner.py [-c config.json] [-o output.json] [-v]
|
||||||
|
|
||||||
|
2. pfsense_scanner.py
|
||||||
|
Specialized scanner for pfSense firewalls
|
||||||
|
Usage: ./pfsense_scanner.py <ip> [-u user] [-k keyfile] [-o output.json]
|
||||||
|
|
||||||
|
3. integrated_scanner.py
|
||||||
|
Complete scanner with pfSense integration
|
||||||
|
Usage: ./integrated_scanner.py [-c config.json] [-o output.json] [-v]
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
-------------
|
||||||
|
Edit config.json to customize:
|
||||||
|
- SSH credentials
|
||||||
|
- Network ranges to scan
|
||||||
|
- Special device definitions
|
||||||
|
- Scan timeouts
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
--------
|
||||||
|
# Quick scan of current network
|
||||||
|
./network_scanner.py -v
|
||||||
|
|
||||||
|
# Full scan with diagram
|
||||||
|
./integrated_scanner.py --generate-svg
|
||||||
|
|
||||||
|
# Scan pfSense
|
||||||
|
./pfsense_scanner.py 192.168.1.1 -u root -k ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
For more information, see README.md
|
||||||
|
HELP
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo "Goodbye!"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid option"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo "Thanks for using Network Scanner!"
|
||||||
|
echo "================================"
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Comprehensive Network Diagram Generator
|
|
||||||
Combines network scanning with pfSense XML parsing for complete network topology
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
from network_scanner import NetworkScanner
|
|
||||||
from pfsense_xml_parser import PfSenseXMLParser
|
|
||||||
from svg_generator import NetworkDiagramGenerator
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ComprehensiveNetworkMapper:
|
|
||||||
"""Complete network mapping solution combining multiple data sources"""
|
|
||||||
|
|
||||||
def __init__(self, config: Dict):
|
|
||||||
self.config = config
|
|
||||||
self.network_data = {}
|
|
||||||
self.pfsense_data = {}
|
|
||||||
self.combined_data = {
|
|
||||||
'scan_timestamp': None,
|
|
||||||
'data_sources': [],
|
|
||||||
'segments': [],
|
|
||||||
'pfsense_firewalls': [],
|
|
||||||
'wireguard_networks': [],
|
|
||||||
'openvpn_networks': [],
|
|
||||||
'routing_table': [],
|
|
||||||
'dhcp_leases': [],
|
|
||||||
'static_mappings': []
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_network_scan(self, scan_file: str):
|
|
||||||
"""Load network scan data"""
|
|
||||||
logger.info(f"Loading network scan data from {scan_file}")
|
|
||||||
try:
|
|
||||||
with open(scan_file, 'r') as f:
|
|
||||||
self.network_data = json.load(f)
|
|
||||||
self.combined_data['data_sources'].append('network_scan')
|
|
||||||
logger.info("Network scan data loaded successfully")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error loading network scan: {e}")
|
|
||||||
|
|
||||||
def load_pfsense_configs(self, xml_files: List[str]):
|
|
||||||
"""Load and parse pfSense XML configuration files"""
|
|
||||||
for xml_file in xml_files:
|
|
||||||
if Path(xml_file).exists():
|
|
||||||
logger.info(f"Parsing pfSense config: {xml_file}")
|
|
||||||
parser = PfSenseXMLParser(xml_file)
|
|
||||||
data = parser.parse_all()
|
|
||||||
|
|
||||||
if data:
|
|
||||||
hostname = data.get('hostname', f'pfsense_{len(self.pfsense_data)}')
|
|
||||||
self.pfsense_data[hostname] = data
|
|
||||||
self.combined_data['data_sources'].append(f'pfsense_{hostname}')
|
|
||||||
self.combined_data['pfsense_firewalls'].append(data)
|
|
||||||
logger.info(f"pfSense config {hostname} loaded successfully")
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to parse pfSense config: {xml_file}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"pfSense config file not found: {xml_file}")
|
|
||||||
|
|
||||||
def merge_network_data(self):
|
|
||||||
"""Merge all data sources into comprehensive network map"""
|
|
||||||
logger.info("Merging network data sources...")
|
|
||||||
|
|
||||||
# Start with network scan data if available
|
|
||||||
if self.network_data:
|
|
||||||
self.combined_data.update({
|
|
||||||
'scan_timestamp': self.network_data.get('scan_timestamp'),
|
|
||||||
'segments': self.network_data.get('segments', [])
|
|
||||||
})
|
|
||||||
|
|
||||||
# Process pfSense data
|
|
||||||
self._process_pfsense_data()
|
|
||||||
|
|
||||||
# Extract additional network information
|
|
||||||
self._extract_network_topology()
|
|
||||||
|
|
||||||
logger.info("Network data merging complete")
|
|
||||||
|
|
||||||
def _process_pfsense_data(self):
|
|
||||||
"""Process and integrate pfSense configuration data"""
|
|
||||||
for hostname, pfsense in self.pfsense_data.items():
|
|
||||||
logger.info(f"Processing pfSense data for {hostname}")
|
|
||||||
|
|
||||||
# Add interfaces as network segments
|
|
||||||
interfaces = pfsense.get('interfaces', {})
|
|
||||||
for iface_name, iface_data in interfaces.items():
|
|
||||||
if iface_data.get('ipaddr') and iface_data.get('ipaddr') != 'dhcp':
|
|
||||||
try:
|
|
||||||
# Create network segment from interface
|
|
||||||
ip = iface_data['ipaddr']
|
|
||||||
subnet = iface_data.get('subnet', '24')
|
|
||||||
network_cidr = f"{ip.rsplit('.', 1)[0]}.0/{subnet}"
|
|
||||||
|
|
||||||
segment = {
|
|
||||||
'name': f"{hostname}_{iface_name}",
|
|
||||||
'cidr': network_cidr,
|
|
||||||
'gateway': iface_data.get('gateway', ip),
|
|
||||||
'interface': iface_data.get('interface'),
|
|
||||||
'pfsense_host': hostname,
|
|
||||||
'is_vpn': iface_data.get('type') in ['wireguard', 'openvpn'],
|
|
||||||
'devices': []
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add gateway device
|
|
||||||
gateway_device = {
|
|
||||||
'ip': ip,
|
|
||||||
'hostname': hostname,
|
|
||||||
'device_type': 'firewall',
|
|
||||||
'os_type': 'pfSense',
|
|
||||||
'interface': iface_name,
|
|
||||||
'pfsense_config': True
|
|
||||||
}
|
|
||||||
segment['devices'].append(gateway_device)
|
|
||||||
|
|
||||||
self.combined_data['segments'].append(segment)
|
|
||||||
|
|
||||||
# Track VPN networks
|
|
||||||
if iface_data.get('type') == 'wireguard':
|
|
||||||
self.combined_data['wireguard_networks'].append({
|
|
||||||
'network': network_cidr,
|
|
||||||
'interface': iface_data.get('interface'),
|
|
||||||
'pfsense': hostname,
|
|
||||||
'peers': pfsense.get('wireguard', {}).get('peers', [])
|
|
||||||
})
|
|
||||||
elif iface_data.get('type') == 'openvpn':
|
|
||||||
self.combined_data['openvpn_networks'].append({
|
|
||||||
'network': network_cidr,
|
|
||||||
'interface': iface_data.get('interface'),
|
|
||||||
'pfsense': hostname
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Error processing interface {iface_name}: {e}")
|
|
||||||
|
|
||||||
# Add static routes
|
|
||||||
static_routes = pfsense.get('static_routes', [])
|
|
||||||
for route in static_routes:
|
|
||||||
route_info = {
|
|
||||||
'network': route.get('network'),
|
|
||||||
'gateway': route.get('gateway'),
|
|
||||||
'description': route.get('description'),
|
|
||||||
'pfsense': hostname
|
|
||||||
}
|
|
||||||
self.combined_data['routing_table'].append(route_info)
|
|
||||||
|
|
||||||
# Add DHCP information
|
|
||||||
dhcp_config = pfsense.get('dhcp', {})
|
|
||||||
for iface_name, dhcp_data in dhcp_config.items():
|
|
||||||
if dhcp_data.get('static_mappings'):
|
|
||||||
for mapping in dhcp_data['static_mappings']:
|
|
||||||
mapping_info = {
|
|
||||||
'ip': mapping.get('ipaddr'),
|
|
||||||
'mac': mapping.get('mac'),
|
|
||||||
'hostname': mapping.get('hostname'),
|
|
||||||
'description': mapping.get('description'),
|
|
||||||
'interface': iface_name,
|
|
||||||
'pfsense': hostname
|
|
||||||
}
|
|
||||||
self.combined_data['static_mappings'].append(mapping_info)
|
|
||||||
|
|
||||||
def _extract_network_topology(self):
|
|
||||||
"""Extract network topology information from all sources"""
|
|
||||||
logger.info("Extracting network topology...")
|
|
||||||
|
|
||||||
# Process WireGuard peer information
|
|
||||||
for wg_net in self.combined_data['wireguard_networks']:
|
|
||||||
for peer in wg_net.get('peers', []):
|
|
||||||
for allowed_ip in peer.get('allowed_ips', []):
|
|
||||||
# Create segment for remote networks
|
|
||||||
if allowed_ip.get('mask') and allowed_ip['mask'] != '32':
|
|
||||||
remote_segment = {
|
|
||||||
'name': f"WG_Remote_{allowed_ip['address']}_{allowed_ip['mask']}",
|
|
||||||
'cidr': f"{allowed_ip['address']}/{allowed_ip['mask']}",
|
|
||||||
'gateway': wg_net.get('network', '').split('/')[0].rsplit('.', 1)[0] + '.1',
|
|
||||||
'is_vpn': True,
|
|
||||||
'vpn_type': 'wireguard',
|
|
||||||
'pfsense_host': wg_net.get('pfsense'),
|
|
||||||
'devices': []
|
|
||||||
}
|
|
||||||
self.combined_data['segments'].append(remote_segment)
|
|
||||||
|
|
||||||
def generate_svg_diagram(self, output_file: str):
|
|
||||||
"""Generate SVG network diagram"""
|
|
||||||
logger.info(f"Generating SVG diagram: {output_file}")
|
|
||||||
|
|
||||||
# Use the existing SVG generator
|
|
||||||
generator = NetworkDiagramGenerator(self.combined_data)
|
|
||||||
generator.generate_svg(output_file)
|
|
||||||
|
|
||||||
logger.info(f"SVG diagram generated: {output_file}")
|
|
||||||
|
|
||||||
def export_comprehensive_json(self, output_file: str):
|
|
||||||
"""Export comprehensive network data"""
|
|
||||||
logger.info(f"Exporting comprehensive data to {output_file}")
|
|
||||||
|
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(self.combined_data, f, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
logger.info(f"Comprehensive network data exported to {output_file}")
|
|
||||||
|
|
||||||
def print_summary(self):
|
|
||||||
"""Print comprehensive network summary"""
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("COMPREHENSIVE NETWORK MAPPING SUMMARY")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
print(f"\nData Sources: {', '.join(self.combined_data['data_sources'])}")
|
|
||||||
print(f"Network Segments: {len(self.combined_data['segments'])}")
|
|
||||||
print(f"pfSense Firewalls: {len(self.combined_data['pfsense_firewalls'])}")
|
|
||||||
print(f"WireGuard Networks: {len(self.combined_data['wireguard_networks'])}")
|
|
||||||
print(f"OpenVPN Networks: {len(self.combined_data['openvpn_networks'])}")
|
|
||||||
print(f"Static Routes: {len(self.combined_data['routing_table'])}")
|
|
||||||
print(f"DHCP Static Mappings: {len(self.combined_data['static_mappings'])}")
|
|
||||||
|
|
||||||
# Network segments summary
|
|
||||||
print(f"\n{'='*80}")
|
|
||||||
print("NETWORK SEGMENTS")
|
|
||||||
print(f"{'='*80}")
|
|
||||||
|
|
||||||
for segment in self.combined_data['segments']:
|
|
||||||
vpn_indicator = " (VPN)" if segment.get('is_vpn') else ""
|
|
||||||
pfsense_indicator = f" [{segment.get('pfsense_host')}]" if segment.get('pfsense_host') else ""
|
|
||||||
print(f"\n📡 {segment['name']}{vpn_indicator}{pfsense_indicator}")
|
|
||||||
print(f" CIDR: {segment['cidr']}")
|
|
||||||
print(f" Gateway: {segment.get('gateway', 'N/A')}")
|
|
||||||
print(f" Devices: {len(segment.get('devices', []))}")
|
|
||||||
|
|
||||||
# Show devices
|
|
||||||
for device in segment.get('devices', [])[:5]: # Show first 5
|
|
||||||
print(f" • {device.get('ip', 'N/A')} - {device.get('hostname', 'N/A')} ({device.get('device_type', 'N/A')})")
|
|
||||||
|
|
||||||
if len(segment.get('devices', [])) > 5:
|
|
||||||
print(f" ... and {len(segment.get('devices', [])) - 5} more devices")
|
|
||||||
|
|
||||||
# pfSense summary
|
|
||||||
if self.combined_data['pfsense_firewalls']:
|
|
||||||
print(f"\n{'='*80}")
|
|
||||||
print("PFSENSE FIREWALLS")
|
|
||||||
print(f"{'='*80}")
|
|
||||||
|
|
||||||
for pfsense in self.combined_data['pfsense_firewalls']:
|
|
||||||
hostname = pfsense.get('hostname', 'Unknown')
|
|
||||||
print(f"\n🛡️ {hostname}")
|
|
||||||
print(f" Interfaces: {len(pfsense.get('interfaces', {}))}")
|
|
||||||
print(f" Static Routes: {len(pfsense.get('static_routes', []))}")
|
|
||||||
|
|
||||||
wg_config = pfsense.get('wireguard', {})
|
|
||||||
if wg_config.get('enabled'):
|
|
||||||
print(f" WireGuard: {len(wg_config.get('tunnels', []))} tunnels, {len(wg_config.get('peers', []))} peers")
|
|
||||||
|
|
||||||
dhcp_config = pfsense.get('dhcp', {})
|
|
||||||
total_mappings = sum(len(dhcp.get('static_mappings', [])) for dhcp in dhcp_config.values())
|
|
||||||
print(f" DHCP Static Mappings: {total_mappings}")
|
|
||||||
|
|
||||||
# VPN Networks
|
|
||||||
if self.combined_data['wireguard_networks'] or self.combined_data['openvpn_networks']:
|
|
||||||
print(f"\n{'='*80}")
|
|
||||||
print("VPN NETWORKS")
|
|
||||||
print(f"{'='*80}")
|
|
||||||
|
|
||||||
for wg_net in self.combined_data['wireguard_networks']:
|
|
||||||
print(f"\n🔐 WireGuard: {wg_net['network']} via {wg_net['pfsense']}")
|
|
||||||
print(f" Interface: {wg_net['interface']}")
|
|
||||||
print(f" Peers: {len(wg_net.get('peers', []))}")
|
|
||||||
|
|
||||||
for ovpn_net in self.combined_data['openvpn_networks']:
|
|
||||||
print(f"\n🔐 OpenVPN: {ovpn_net['network']} via {ovpn_net['pfsense']}")
|
|
||||||
print(f" Interface: {ovpn_net['interface']}")
|
|
||||||
|
|
||||||
print(f"\n{'='*80}")
|
|
||||||
print("NETWORK TOPOLOGY ANALYSIS COMPLETE")
|
|
||||||
print(f"{'='*80}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Command line interface"""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Comprehensive Network Diagram Generator with pfSense Integration'
|
|
||||||
)
|
|
||||||
parser.add_argument('-c', '--config', default='config.json',
|
|
||||||
help='Network scanner configuration file')
|
|
||||||
parser.add_argument('-s', '--scan-data', help='Existing network scan JSON file')
|
|
||||||
parser.add_argument('-p', '--pfsense-xml', nargs='+',
|
|
||||||
help='pfSense XML configuration files')
|
|
||||||
parser.add_argument('-o', '--output', default='comprehensive_network.json',
|
|
||||||
help='Output comprehensive JSON file')
|
|
||||||
parser.add_argument('--svg', help='Generate SVG diagram')
|
|
||||||
parser.add_argument('-v', '--verbose', action='store_true',
|
|
||||||
help='Verbose output')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
# Load configuration
|
|
||||||
config = {}
|
|
||||||
if Path(args.config).exists():
|
|
||||||
try:
|
|
||||||
with open(args.config, 'r') as f:
|
|
||||||
config = json.load(f)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Could not load config {args.config}: {e}")
|
|
||||||
|
|
||||||
# Initialize comprehensive mapper
|
|
||||||
mapper = ComprehensiveNetworkMapper(config)
|
|
||||||
|
|
||||||
# Load network scan data if provided
|
|
||||||
if args.scan_data and Path(args.scan_data).exists():
|
|
||||||
mapper.load_network_scan(args.scan_data)
|
|
||||||
else:
|
|
||||||
logger.info("No network scan data provided - will use pfSense data only")
|
|
||||||
|
|
||||||
# Load pfSense configurations
|
|
||||||
pfsense_files = args.pfsense_xml or []
|
|
||||||
# Auto-detect pfSense XML files if none specified
|
|
||||||
if not pfsense_files:
|
|
||||||
xml_files = list(Path('.').glob('config-*.xml'))
|
|
||||||
if xml_files:
|
|
||||||
pfsense_files = [str(f) for f in xml_files]
|
|
||||||
logger.info(f"Auto-detected pfSense XML files: {pfsense_files}")
|
|
||||||
|
|
||||||
if pfsense_files:
|
|
||||||
mapper.load_pfsense_configs(pfsense_files)
|
|
||||||
else:
|
|
||||||
logger.warning("No pfSense XML files found")
|
|
||||||
|
|
||||||
# Merge all data
|
|
||||||
mapper.merge_network_data()
|
|
||||||
|
|
||||||
# Print summary
|
|
||||||
mapper.print_summary()
|
|
||||||
|
|
||||||
# Export comprehensive data
|
|
||||||
mapper.export_comprehensive_json(args.output)
|
|
||||||
print(f"\n✅ Comprehensive network data saved to: {args.output}")
|
|
||||||
|
|
||||||
# Generate SVG if requested
|
|
||||||
if args.svg:
|
|
||||||
svg_file = args.svg
|
|
||||||
mapper.generate_svg_diagram(svg_file)
|
|
||||||
print(f"✅ SVG network diagram generated: {svg_file}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -447,8 +447,6 @@ def main():
|
|||||||
help='Output JSON file (default: network_scan.json)')
|
help='Output JSON file (default: network_scan.json)')
|
||||||
parser.add_argument('-v', '--verbose', action='store_true',
|
parser.add_argument('-v', '--verbose', action='store_true',
|
||||||
help='Verbose output')
|
help='Verbose output')
|
||||||
parser.add_argument('--generate-svg', action='store_true',
|
|
||||||
help='Automatically generate SVG diagram after scan')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -474,23 +472,15 @@ def main():
|
|||||||
# Export results
|
# Export results
|
||||||
scanner.export_json(args.output)
|
scanner.export_json(args.output)
|
||||||
|
|
||||||
|
# Save failed SSH hosts if any
|
||||||
|
if scanner.base_scanner.failed_ssh_hosts:
|
||||||
|
failed_ssh_file = args.output.replace('.json', '_failed_ssh.json')
|
||||||
|
scanner.base_scanner.save_failed_ssh_hosts(failed_ssh_file)
|
||||||
|
print(f"\n⚠️ {len(scanner.base_scanner.failed_ssh_hosts)} hosts have SSH port open but failed authentication.")
|
||||||
|
print(f" See {failed_ssh_file} for details.")
|
||||||
|
|
||||||
print(f"\n✓ Scan complete! Results saved to {args.output}")
|
print(f"\n✓ Scan complete! Results saved to {args.output}")
|
||||||
|
|
||||||
# Generate SVG if requested
|
|
||||||
if args.generate_svg:
|
|
||||||
try:
|
|
||||||
from svg_generator import NetworkDiagramGenerator
|
|
||||||
|
|
||||||
with open(args.output, 'r') as f:
|
|
||||||
scan_data = json.load(f)
|
|
||||||
|
|
||||||
svg_file = args.output.replace('.json', '.svg')
|
|
||||||
generator = NetworkDiagramGenerator(scan_data)
|
|
||||||
generator.generate_svg(svg_file)
|
|
||||||
|
|
||||||
print(f"✓ SVG diagram generated: {svg_file}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error generating SVG: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
from dataclasses import dataclass, asdict, field
|
from dataclasses import dataclass, asdict, field
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -40,6 +41,7 @@ class Device:
|
|||||||
services: List[str] = field(default_factory=list)
|
services: List[str] = field(default_factory=list)
|
||||||
routes: List[Dict] = field(default_factory=list)
|
routes: List[Dict] = field(default_factory=list)
|
||||||
interfaces: List[Dict] = field(default_factory=list)
|
interfaces: List[Dict] = field(default_factory=list)
|
||||||
|
ssh_info: Optional[Dict] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -62,6 +64,7 @@ class NetworkScanner:
|
|||||||
self.ssh_user = config.get('ssh_user', 'root')
|
self.ssh_user = config.get('ssh_user', 'root')
|
||||||
self.ssh_key = config.get('ssh_key_path')
|
self.ssh_key = config.get('ssh_key_path')
|
||||||
self.timeout = config.get('timeout', 2)
|
self.timeout = config.get('timeout', 2)
|
||||||
|
self.failed_ssh_hosts: List[Dict] = []
|
||||||
|
|
||||||
def discover_networks(self) -> List[str]:
|
def discover_networks(self) -> List[str]:
|
||||||
"""Discover all network segments from local routing table"""
|
"""Discover all network segments from local routing table"""
|
||||||
@@ -198,6 +201,13 @@ class NetworkScanner:
|
|||||||
if device.ssh_accessible:
|
if device.ssh_accessible:
|
||||||
# Gather detailed info via SSH
|
# Gather detailed info via SSH
|
||||||
self._gather_ssh_info(device)
|
self._gather_ssh_info(device)
|
||||||
|
else:
|
||||||
|
# Track hosts with SSH port open but failed SSH connection
|
||||||
|
self.failed_ssh_hosts.append({
|
||||||
|
'ip': ip,
|
||||||
|
'hostname': device.hostname,
|
||||||
|
'reason': 'SSH port open but authentication failed or connection refused'
|
||||||
|
})
|
||||||
|
|
||||||
# Identify device type based on ports and services
|
# Identify device type based on ports and services
|
||||||
device.device_type = self._identify_device_type(device)
|
device.device_type = self._identify_device_type(device)
|
||||||
@@ -290,17 +300,41 @@ class NetworkScanner:
|
|||||||
"""Gather detailed information via SSH"""
|
"""Gather detailed information via SSH"""
|
||||||
logger.info(f"Gathering SSH info from {device.ip}")
|
logger.info(f"Gathering SSH info from {device.ip}")
|
||||||
|
|
||||||
|
ssh_info = {}
|
||||||
|
|
||||||
# Get OS info
|
# Get OS info
|
||||||
device.os_type, device.os_version = self._get_os_info(device.ip)
|
os_type, os_version = self._get_os_info(device.ip)
|
||||||
|
if os_type:
|
||||||
|
ssh_info['os_type'] = os_type
|
||||||
|
device.os_type = os_type
|
||||||
|
if os_version:
|
||||||
|
ssh_info['os_version'] = os_version
|
||||||
|
device.os_version = os_version
|
||||||
|
|
||||||
# Get network interfaces
|
# Get network interfaces
|
||||||
device.interfaces = self._get_interfaces(device.ip)
|
interfaces = self._get_interfaces(device.ip)
|
||||||
|
if interfaces:
|
||||||
|
ssh_info['interfaces'] = interfaces
|
||||||
|
device.interfaces = interfaces
|
||||||
|
|
||||||
# Get routing table
|
# Get routing table
|
||||||
device.routes = self._get_routes(device.ip)
|
routes = self._get_routes(device.ip)
|
||||||
|
if routes:
|
||||||
|
ssh_info['routes'] = routes
|
||||||
|
device.routes = routes
|
||||||
|
|
||||||
# Get running services
|
# Get running services
|
||||||
device.services = self._get_services(device.ip)
|
services = self._get_services(device.ip)
|
||||||
|
if services:
|
||||||
|
ssh_info['services'] = services
|
||||||
|
device.services = services
|
||||||
|
|
||||||
|
# Get system info
|
||||||
|
system_info = self._get_system_info(device.ip)
|
||||||
|
if system_info:
|
||||||
|
ssh_info['system'] = system_info
|
||||||
|
|
||||||
|
device.ssh_info = ssh_info
|
||||||
|
|
||||||
def _ssh_exec(self, ip: str, command: str) -> Optional[str]:
|
def _ssh_exec(self, ip: str, command: str) -> Optional[str]:
|
||||||
"""Execute command via SSH"""
|
"""Execute command via SSH"""
|
||||||
@@ -409,6 +443,40 @@ class NetworkScanner:
|
|||||||
|
|
||||||
return services[:20] # Limit to top 20
|
return services[:20] # Limit to top 20
|
||||||
|
|
||||||
|
def _get_system_info(self, ip: str) -> Optional[Dict]:
|
||||||
|
"""Get basic system information"""
|
||||||
|
system_info = {}
|
||||||
|
|
||||||
|
# Get hostname
|
||||||
|
hostname = self._ssh_exec(ip, 'hostname')
|
||||||
|
if hostname:
|
||||||
|
system_info['hostname'] = hostname.strip()
|
||||||
|
|
||||||
|
# Get uptime
|
||||||
|
uptime = self._ssh_exec(ip, 'uptime -p 2>/dev/null || uptime')
|
||||||
|
if uptime:
|
||||||
|
system_info['uptime'] = uptime.strip()
|
||||||
|
|
||||||
|
# Get CPU info
|
||||||
|
cpu_info = self._ssh_exec(ip, 'nproc && cat /proc/cpuinfo | grep "model name" | head -1')
|
||||||
|
if cpu_info:
|
||||||
|
lines = cpu_info.strip().split('\n')
|
||||||
|
if len(lines) >= 2:
|
||||||
|
system_info['cpu_cores'] = int(lines[0])
|
||||||
|
system_info['cpu_model'] = lines[1].split(':')[1].strip()
|
||||||
|
|
||||||
|
# Get memory info
|
||||||
|
mem_info = self._ssh_exec(ip, 'free -h | grep Mem')
|
||||||
|
if mem_info:
|
||||||
|
system_info['memory'] = mem_info.strip()
|
||||||
|
|
||||||
|
# Get disk info
|
||||||
|
disk_info = self._ssh_exec(ip, 'df -h / | tail -1')
|
||||||
|
if disk_info:
|
||||||
|
system_info['disk'] = disk_info.strip()
|
||||||
|
|
||||||
|
return system_info if system_info else None
|
||||||
|
|
||||||
def _identify_device_type(self, device: Device) -> str:
|
def _identify_device_type(self, device: Device) -> str:
|
||||||
"""Identify device type based on available info"""
|
"""Identify device type based on available info"""
|
||||||
if device.routes and len(device.routes) > 5:
|
if device.routes and len(device.routes) > 5:
|
||||||
@@ -505,6 +573,17 @@ class NetworkScanner:
|
|||||||
|
|
||||||
logger.info(f"Exported results to {filename}")
|
logger.info(f"Exported results to {filename}")
|
||||||
|
|
||||||
|
def save_failed_ssh_hosts(self, filename: str):
|
||||||
|
"""Save list of hosts with SSH port open but failed authentication"""
|
||||||
|
if self.failed_ssh_hosts:
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'failed_ssh_hosts': self.failed_ssh_hosts,
|
||||||
|
'total_failed': len(self.failed_ssh_hosts),
|
||||||
|
'scan_timestamp': str(datetime.now())
|
||||||
|
}, f, indent=2)
|
||||||
|
logger.info(f"Saved {len(self.failed_ssh_hosts)} failed SSH hosts to {filename}")
|
||||||
|
|
||||||
def print_summary(self):
|
def print_summary(self):
|
||||||
"""Print a human-readable summary"""
|
"""Print a human-readable summary"""
|
||||||
print("\n" + "="*80)
|
print("\n" + "="*80)
|
||||||
@@ -567,6 +646,7 @@ def main():
|
|||||||
# Export results
|
# Export results
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
scanner.export_json(args.output)
|
scanner.export_json(args.output)
|
||||||
|
scanner.save_failed_ssh_hosts('failed_ssh_hosts.json')
|
||||||
|
|
||||||
print(f"\n✓ Scan complete! Results saved to {args.output}")
|
print(f"\n✓ Scan complete! Results saved to {args.output}")
|
||||||
|
|
||||||
|
|||||||
385
src/server_info_collector.py
Normal file
385
src/server_info_collector.py
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Server Information Collector
|
||||||
|
|
||||||
|
This module collects detailed information about servers and VMs from hypervisors
|
||||||
|
via SSH. It connects to specified hypervisors and gathers system information,
|
||||||
|
VM details, resource usage, and network configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VMInfo:
|
||||||
|
"""Information about a virtual machine."""
|
||||||
|
vmid: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
cpu: float
|
||||||
|
memory_used: int
|
||||||
|
memory_total: int
|
||||||
|
disk_used: int
|
||||||
|
disk_total: int
|
||||||
|
uptime: str
|
||||||
|
ip_addresses: List[str]
|
||||||
|
vm_type: str = 'vm' # 'vm' or 'container'
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServerInfo:
|
||||||
|
"""Information about a physical server/hypervisor."""
|
||||||
|
hostname: str
|
||||||
|
os: str
|
||||||
|
kernel: str
|
||||||
|
uptime: str
|
||||||
|
cpu_model: str
|
||||||
|
cpu_cores: int
|
||||||
|
memory_total: int
|
||||||
|
memory_free: int
|
||||||
|
disk_total: int
|
||||||
|
disk_free: int
|
||||||
|
load_average: str
|
||||||
|
network_interfaces: Dict[str, str]
|
||||||
|
vms: List[VMInfo]
|
||||||
|
containers: Optional[List[VMInfo]] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HypervisorConfig:
|
||||||
|
"""Configuration for a hypervisor connection."""
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
user: str
|
||||||
|
|
||||||
|
class ServerInfoCollector:
|
||||||
|
"""Collects server and VM information from hypervisors via SSH."""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]):
|
||||||
|
self.config = config
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.hypervisors = [
|
||||||
|
HypervisorConfig(**hv) for hv in config.get('hypervisors', [])
|
||||||
|
]
|
||||||
|
self.ssh_user = config.get('ssh_user', 'root')
|
||||||
|
self.ssh_key_path = config.get('ssh_key_path')
|
||||||
|
self.timeout = config.get('timeout', 10)
|
||||||
|
|
||||||
|
def run_ssh_command(self, host: str, port: int, user: str, command: str) -> Optional[str]:
|
||||||
|
"""Run a command via SSH on a remote host."""
|
||||||
|
ssh_cmd = [
|
||||||
|
'ssh',
|
||||||
|
'-o', 'ConnectTimeout=5',
|
||||||
|
'-o', 'StrictHostKeyChecking=no',
|
||||||
|
'-o', 'UserKnownHostsFile=/dev/null',
|
||||||
|
'-p', str(port),
|
||||||
|
f'{user}@{host}',
|
||||||
|
command
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
ssh_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return result.stdout.strip()
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"SSH command failed on {host}:{port}: {result.stderr}")
|
||||||
|
return None
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.logger.warning(f"SSH command timed out on {host}:{port}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"SSH error on {host}:{port}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def detect_hypervisor_type(self, host: str, port: int, user: str) -> str:
|
||||||
|
"""Detect the type of hypervisor (Proxmox, VMware, etc.)."""
|
||||||
|
# Check for Proxmox
|
||||||
|
if self.run_ssh_command(host, port, user, 'which pvesh'):
|
||||||
|
return 'proxmox'
|
||||||
|
|
||||||
|
# Check for VMware
|
||||||
|
if self.run_ssh_command(host, port, user, 'which vmware'):
|
||||||
|
return 'vmware'
|
||||||
|
|
||||||
|
# Check for KVM/libvirt
|
||||||
|
if self.run_ssh_command(host, port, user, 'which virsh'):
|
||||||
|
return 'kvm'
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
def collect_proxmox_info(self, host: str, port: int, user: str) -> ServerInfo:
|
||||||
|
"""Collect information from a Proxmox hypervisor."""
|
||||||
|
hostname = self.run_ssh_command(host, port, user, 'hostname') or host
|
||||||
|
|
||||||
|
# System info
|
||||||
|
os_info = self.run_ssh_command(host, port, user, 'cat /etc/os-release | grep PRETTY_NAME | cut -d\'"\' -f2') or 'Unknown'
|
||||||
|
kernel = self.run_ssh_command(host, port, user, 'uname -r') or 'Unknown'
|
||||||
|
uptime = self.run_ssh_command(host, port, user, 'uptime -p') or 'Unknown'
|
||||||
|
|
||||||
|
# CPU info
|
||||||
|
cpu_model = self.run_ssh_command(host, port, user, 'cat /proc/cpuinfo | grep "model name" | head -1 | cut -d: -f2 | xargs') or 'Unknown'
|
||||||
|
cpu_cores = int(self.run_ssh_command(host, port, user, 'nproc') or '0')
|
||||||
|
|
||||||
|
# Memory info
|
||||||
|
mem_info = self.run_ssh_command(host, port, user, 'free -b | grep Mem')
|
||||||
|
if mem_info:
|
||||||
|
parts = mem_info.split()
|
||||||
|
memory_total = int(parts[1])
|
||||||
|
memory_free = int(parts[3])
|
||||||
|
else:
|
||||||
|
memory_total = memory_free = 0
|
||||||
|
|
||||||
|
# Disk info
|
||||||
|
disk_info = self.run_ssh_command(host, port, user, 'df -B1 / | tail -1')
|
||||||
|
if disk_info:
|
||||||
|
parts = disk_info.split()
|
||||||
|
disk_total = int(parts[1])
|
||||||
|
disk_free = int(parts[3])
|
||||||
|
else:
|
||||||
|
disk_total = disk_free = 0
|
||||||
|
|
||||||
|
# Load average
|
||||||
|
load_average = self.run_ssh_command(host, port, user, 'uptime | cut -d: -f5') or 'Unknown'
|
||||||
|
|
||||||
|
# Network interfaces
|
||||||
|
network_interfaces = {}
|
||||||
|
net_info = self.run_ssh_command(host, port, user, 'ip -o addr show | grep -v lo')
|
||||||
|
if net_info:
|
||||||
|
for line in net_info.split('\n'):
|
||||||
|
if line.strip():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
iface = parts[1]
|
||||||
|
ip = parts[3].split('/')[0]
|
||||||
|
network_interfaces[iface] = ip
|
||||||
|
|
||||||
|
# VMs
|
||||||
|
vms = []
|
||||||
|
vm_list = self.run_ssh_command(host, port, user, 'qm list')
|
||||||
|
if vm_list:
|
||||||
|
for line in vm_list.split('\n')[1:]: # Skip header
|
||||||
|
if line.strip():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 3:
|
||||||
|
vmid = parts[0]
|
||||||
|
name = parts[1]
|
||||||
|
status = parts[2]
|
||||||
|
|
||||||
|
# Get VM resource usage
|
||||||
|
cpu = memory_used = memory_total = disk_used = disk_total = 0
|
||||||
|
uptime = 'Unknown'
|
||||||
|
|
||||||
|
# Try to get current status
|
||||||
|
status_output = self.run_ssh_command(host, port, user, f'qm status {vmid} --verbose')
|
||||||
|
if status_output:
|
||||||
|
# Parse status output for resource usage using grep-like extraction
|
||||||
|
if 'mem:' in status_output:
|
||||||
|
try:
|
||||||
|
mem_line = [line for line in status_output.split('\n') if line.strip().startswith('mem:')][0]
|
||||||
|
memory_used = int(mem_line.split(':')[1].strip())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'maxmem:' in status_output:
|
||||||
|
try:
|
||||||
|
maxmem_line = [line for line in status_output.split('\n') if line.strip().startswith('maxmem:')][0]
|
||||||
|
memory_total = int(maxmem_line.split(':')[1].strip())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'maxdisk:' in status_output:
|
||||||
|
try:
|
||||||
|
maxdisk_line = [line for line in status_output.split('\n') if line.strip().startswith('maxdisk:')][0]
|
||||||
|
disk_total = int(maxdisk_line.split(':')[1].strip())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'uptime:' in status_output:
|
||||||
|
try:
|
||||||
|
uptime_line = [line for line in status_output.split('\n') if line.strip().startswith('uptime:')][0]
|
||||||
|
uptime = uptime_line.split(':')[1].strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get VM config for IP addresses
|
||||||
|
vm_config = self.run_ssh_command(host, port, user, f'qm config {vmid}')
|
||||||
|
ip_addresses = []
|
||||||
|
if vm_config:
|
||||||
|
for config_line in vm_config.split('\n'):
|
||||||
|
if 'ip=' in config_line:
|
||||||
|
ip = config_line.split('=')[1].strip()
|
||||||
|
ip_addresses.append(ip)
|
||||||
|
|
||||||
|
vm_info = VMInfo(
|
||||||
|
vmid=vmid,
|
||||||
|
name=name,
|
||||||
|
status=status,
|
||||||
|
cpu=cpu,
|
||||||
|
memory_used=memory_used,
|
||||||
|
memory_total=memory_total,
|
||||||
|
disk_used=disk_used,
|
||||||
|
disk_total=disk_total,
|
||||||
|
uptime=uptime,
|
||||||
|
ip_addresses=ip_addresses,
|
||||||
|
vm_type='vm'
|
||||||
|
)
|
||||||
|
vms.append(vm_info)
|
||||||
|
|
||||||
|
# Containers
|
||||||
|
containers = []
|
||||||
|
ct_list = self.run_ssh_command(host, port, user, 'pct list')
|
||||||
|
if ct_list:
|
||||||
|
for line in ct_list.split('\n')[1:]: # Skip header
|
||||||
|
if line.strip():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 3:
|
||||||
|
vmid = parts[0]
|
||||||
|
status = parts[1]
|
||||||
|
# Handle variable number of columns (Lock column may be empty)
|
||||||
|
if len(parts) == 4:
|
||||||
|
name = parts[3]
|
||||||
|
else:
|
||||||
|
name = parts[2]
|
||||||
|
|
||||||
|
# Get container resource usage
|
||||||
|
cpu = memory_used = memory_total = disk_used = disk_total = 0
|
||||||
|
uptime = 'Unknown'
|
||||||
|
|
||||||
|
# Try to get current status
|
||||||
|
status_output = self.run_ssh_command(host, port, user, f'pct status {vmid}')
|
||||||
|
if status_output:
|
||||||
|
# Parse status output for resource usage
|
||||||
|
lines = status_output.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if ':' in line:
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
if key == 'status':
|
||||||
|
# Status is already from pct list
|
||||||
|
pass
|
||||||
|
# Container resource info might be limited
|
||||||
|
# Could add more parsing here if available
|
||||||
|
|
||||||
|
# Get container config for IP addresses
|
||||||
|
ct_config = self.run_ssh_command(host, port, user, f'pct config {vmid}')
|
||||||
|
ip_addresses = []
|
||||||
|
if ct_config:
|
||||||
|
for config_line in ct_config.split('\n'):
|
||||||
|
if 'ip=' in config_line:
|
||||||
|
ip = config_line.split('=')[1].strip()
|
||||||
|
ip_addresses.append(ip)
|
||||||
|
|
||||||
|
container_info = VMInfo(
|
||||||
|
vmid=vmid,
|
||||||
|
name=name,
|
||||||
|
status=status,
|
||||||
|
cpu=cpu,
|
||||||
|
memory_used=memory_used,
|
||||||
|
memory_total=memory_total,
|
||||||
|
disk_used=disk_used,
|
||||||
|
disk_total=disk_total,
|
||||||
|
uptime=uptime,
|
||||||
|
ip_addresses=ip_addresses,
|
||||||
|
vm_type='container'
|
||||||
|
)
|
||||||
|
containers.append(container_info)
|
||||||
|
|
||||||
|
return ServerInfo(
|
||||||
|
hostname=hostname,
|
||||||
|
os=os_info,
|
||||||
|
kernel=kernel,
|
||||||
|
uptime=uptime,
|
||||||
|
cpu_model=cpu_model,
|
||||||
|
cpu_cores=cpu_cores,
|
||||||
|
memory_total=memory_total,
|
||||||
|
memory_free=memory_free,
|
||||||
|
disk_total=disk_total,
|
||||||
|
disk_free=disk_free,
|
||||||
|
load_average=load_average,
|
||||||
|
network_interfaces=network_interfaces,
|
||||||
|
vms=vms,
|
||||||
|
containers=containers
|
||||||
|
)
|
||||||
|
|
||||||
|
def collect_server_info(self, hypervisor: HypervisorConfig) -> Optional[ServerInfo]:
|
||||||
|
"""Collect information from a single hypervisor."""
|
||||||
|
self.logger.info(f"Collecting info from {hypervisor.host}:{hypervisor.port}")
|
||||||
|
|
||||||
|
hv_type = self.detect_hypervisor_type(hypervisor.host, hypervisor.port, hypervisor.user)
|
||||||
|
|
||||||
|
if hv_type == 'proxmox':
|
||||||
|
return self.collect_proxmox_info(hypervisor.host, hypervisor.port, hypervisor.user)
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Unsupported hypervisor type: {hv_type} on {hypervisor.host}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def collect_all_server_info(self) -> Dict[str, ServerInfo]:
|
||||||
|
"""Collect information from all configured hypervisors."""
|
||||||
|
server_info = {}
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=len(self.hypervisors)) as executor:
|
||||||
|
future_to_hv = {
|
||||||
|
executor.submit(self.collect_server_info, hv): hv
|
||||||
|
for hv in self.hypervisors
|
||||||
|
}
|
||||||
|
|
||||||
|
for future in as_completed(future_to_hv):
|
||||||
|
hv = future_to_hv[future]
|
||||||
|
try:
|
||||||
|
info = future.result()
|
||||||
|
if info:
|
||||||
|
server_info[f"{hv.host}:{hv.port}"] = info
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to collect info from {hv.host}:{hv.port}: {e}")
|
||||||
|
|
||||||
|
return server_info
|
||||||
|
|
||||||
|
def save_to_file(self, server_info: Dict[str, ServerInfo], filename: str):
|
||||||
|
"""Save collected server information to a JSON file."""
|
||||||
|
data = {host: asdict(info) for host, info in server_info.items()}
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
self.logger.info(f"Server information saved to {filename}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function for standalone execution."""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Collect server information from hypervisors')
|
||||||
|
parser.add_argument('-o', '--output', default='server_details.json', help='Output JSON file')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
try:
|
||||||
|
with open('config.json', 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("config.json not found. Please create it from config.json.example")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
collector = ServerInfoCollector(config)
|
||||||
|
server_info = collector.collect_all_server_info()
|
||||||
|
|
||||||
|
if server_info:
|
||||||
|
collector.save_to_file(server_info, args.output)
|
||||||
|
print(f"Collected information from {len(server_info)} hypervisors")
|
||||||
|
else:
|
||||||
|
print("No server information collected")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
SVG Network Diagram Generator
|
|
||||||
Converts network scan results into an SVG network diagram
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
from typing import Dict, List, Tuple
|
|
||||||
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
||||||
from xml.dom import minidom
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkDiagramGenerator:
|
|
||||||
"""Generate SVG diagrams from network scan data"""
|
|
||||||
|
|
||||||
# Device type styling
|
|
||||||
DEVICE_STYLES = {
|
|
||||||
'router': {'color': '#FF6B6B', 'icon': '🔀', 'shape': 'rect'},
|
|
||||||
'firewall': {'color': '#FF0000', 'icon': '🛡️', 'shape': 'rect'},
|
|
||||||
'switch': {'color': '#4ECDC4', 'icon': '🔌', 'shape': 'rect'},
|
|
||||||
'server': {'color': '#45B7D1', 'icon': '🖥️', 'shape': 'rect'},
|
|
||||||
'linux_server': {'color': '#45B7D1', 'icon': '🐧', 'shape': 'rect'},
|
|
||||||
'windows_client': {'color': '#95E1D3', 'icon': '💻', 'shape': 'rect'},
|
|
||||||
'client': {'color': '#A8E6CF', 'icon': '💻', 'shape': 'rect'},
|
|
||||||
'default': {'color': '#CCCCCC', 'icon': '❓', 'shape': 'rect'}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Network segment colors
|
|
||||||
SEGMENT_COLORS = [
|
|
||||||
'#E3F2FD', '#F3E5F5', '#E8F5E9', '#FFF3E0', '#FCE4EC',
|
|
||||||
'#E0F2F1', '#F1F8E9', '#FFF9C4', '#FFE0B2', '#F8BBD0'
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, scan_data: Dict):
|
|
||||||
self.scan_data = scan_data
|
|
||||||
self.width = 1600
|
|
||||||
self.height = 1200
|
|
||||||
self.margin = 50
|
|
||||||
self.device_width = 120
|
|
||||||
self.device_height = 80
|
|
||||||
self.segment_spacing = 200
|
|
||||||
|
|
||||||
def generate_svg(self, output_file: str):
|
|
||||||
"""Generate SVG diagram"""
|
|
||||||
svg = Element('svg', {
|
|
||||||
'width': str(self.width),
|
|
||||||
'height': str(self.height),
|
|
||||||
'xmlns': 'http://www.w3.org/2000/svg',
|
|
||||||
'version': '1.1'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Add styles
|
|
||||||
self._add_styles(svg)
|
|
||||||
|
|
||||||
# Add title
|
|
||||||
self._add_title(svg)
|
|
||||||
|
|
||||||
# Calculate layout
|
|
||||||
segments = self.scan_data.get('segments', [])
|
|
||||||
layout = self._calculate_layout(segments)
|
|
||||||
|
|
||||||
# Draw network segments
|
|
||||||
for i, (segment, positions) in enumerate(zip(segments, layout)):
|
|
||||||
self._draw_segment(svg, segment, positions, i)
|
|
||||||
|
|
||||||
# Draw connections
|
|
||||||
self._draw_connections(svg, segments, layout)
|
|
||||||
|
|
||||||
# Add legend
|
|
||||||
self._add_legend(svg)
|
|
||||||
|
|
||||||
# Save to file
|
|
||||||
self._save_svg(svg, output_file)
|
|
||||||
|
|
||||||
def _add_styles(self, svg: Element):
|
|
||||||
"""Add CSS styles"""
|
|
||||||
style = SubElement(svg, 'style')
|
|
||||||
style.text = """
|
|
||||||
.device-box { stroke: #333; stroke-width: 2; }
|
|
||||||
.device-label { font-family: Arial, sans-serif; font-size: 12px; fill: #000; }
|
|
||||||
.device-icon { font-size: 24px; }
|
|
||||||
.segment-box { stroke: #666; stroke-width: 2; fill-opacity: 0.1; }
|
|
||||||
.segment-label { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
|
|
||||||
.connection { stroke: #999; stroke-width: 2; stroke-dasharray: 5,5; }
|
|
||||||
.title { font-family: Arial, sans-serif; font-size: 24px; font-weight: bold; fill: #333; }
|
|
||||||
.info-text { font-family: Arial, sans-serif; font-size: 10px; fill: #666; }
|
|
||||||
.legend-text { font-family: Arial, sans-serif; font-size: 11px; fill: #333; }
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _add_title(self, svg: Element):
|
|
||||||
"""Add diagram title"""
|
|
||||||
title = SubElement(svg, 'text', {
|
|
||||||
'x': str(self.width // 2),
|
|
||||||
'y': '30',
|
|
||||||
'text-anchor': 'middle',
|
|
||||||
'class': 'title'
|
|
||||||
})
|
|
||||||
title.text = 'Network Topology Diagram'
|
|
||||||
|
|
||||||
def _calculate_layout(self, segments: List[Dict]) -> List[List[Tuple]]:
|
|
||||||
"""Calculate positions for all devices"""
|
|
||||||
layout = []
|
|
||||||
|
|
||||||
# Arrange segments horizontally
|
|
||||||
segment_width = (self.width - 2 * self.margin) / len(segments) if segments else self.width
|
|
||||||
|
|
||||||
for seg_idx, segment in enumerate(segments):
|
|
||||||
devices = segment.get('devices', [])
|
|
||||||
positions = []
|
|
||||||
|
|
||||||
# Calculate segment area
|
|
||||||
seg_x = self.margin + seg_idx * segment_width
|
|
||||||
seg_y = self.margin + 60
|
|
||||||
seg_w = segment_width - 20
|
|
||||||
seg_h = self.height - seg_y - self.margin
|
|
||||||
|
|
||||||
# Arrange devices in a grid within segment
|
|
||||||
cols = math.ceil(math.sqrt(len(devices)))
|
|
||||||
rows = math.ceil(len(devices) / cols) if cols > 0 else 1
|
|
||||||
|
|
||||||
device_spacing_x = seg_w / (cols + 1) if cols > 0 else seg_w / 2
|
|
||||||
device_spacing_y = seg_h / (rows + 1) if rows > 0 else seg_h / 2
|
|
||||||
|
|
||||||
for dev_idx, device in enumerate(devices):
|
|
||||||
col = dev_idx % cols
|
|
||||||
row = dev_idx // cols
|
|
||||||
|
|
||||||
x = seg_x + (col + 1) * device_spacing_x
|
|
||||||
y = seg_y + (row + 1) * device_spacing_y
|
|
||||||
|
|
||||||
positions.append((x, y, device))
|
|
||||||
|
|
||||||
layout.append(positions)
|
|
||||||
|
|
||||||
return layout
|
|
||||||
|
|
||||||
def _draw_segment(self, svg: Element, segment: Dict, positions: List[Tuple], seg_idx: int):
|
|
||||||
"""Draw a network segment and its devices"""
|
|
||||||
if not positions:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Calculate segment bounds
|
|
||||||
xs = [pos[0] for pos in positions]
|
|
||||||
ys = [pos[1] for pos in positions]
|
|
||||||
|
|
||||||
min_x = min(xs) - self.device_width
|
|
||||||
max_x = max(xs) + self.device_width
|
|
||||||
min_y = min(ys) - self.device_height
|
|
||||||
max_y = max(ys) + self.device_height
|
|
||||||
|
|
||||||
# Draw segment background
|
|
||||||
color = self.SEGMENT_COLORS[seg_idx % len(self.SEGMENT_COLORS)]
|
|
||||||
SubElement(svg, 'rect', {
|
|
||||||
'x': str(min_x - 20),
|
|
||||||
'y': str(min_y - 40),
|
|
||||||
'width': str(max_x - min_x + 40),
|
|
||||||
'height': str(max_y - min_y + 60),
|
|
||||||
'fill': color,
|
|
||||||
'class': 'segment-box',
|
|
||||||
'rx': '10'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Draw segment label
|
|
||||||
label = SubElement(svg, 'text', {
|
|
||||||
'x': str(min_x),
|
|
||||||
'y': str(min_y - 20),
|
|
||||||
'class': 'segment-label'
|
|
||||||
})
|
|
||||||
vpn_indicator = ' (VPN)' if segment.get('is_vpn') else ''
|
|
||||||
label.text = f"{segment.get('name', 'Unknown')} - {segment.get('cidr', '')}{vpn_indicator}"
|
|
||||||
|
|
||||||
# Draw devices
|
|
||||||
for x, y, device in positions:
|
|
||||||
self._draw_device(svg, device, x, y)
|
|
||||||
|
|
||||||
def _draw_device(self, svg: Element, device: Dict, x: float, y: float):
|
|
||||||
"""Draw a single device"""
|
|
||||||
device_type = device.get('device_type', 'default')
|
|
||||||
style = self.DEVICE_STYLES.get(device_type, self.DEVICE_STYLES['default'])
|
|
||||||
|
|
||||||
# Draw device box
|
|
||||||
box_x = x - self.device_width / 2
|
|
||||||
box_y = y - self.device_height / 2
|
|
||||||
|
|
||||||
SubElement(svg, 'rect', {
|
|
||||||
'x': str(box_x),
|
|
||||||
'y': str(box_y),
|
|
||||||
'width': str(self.device_width),
|
|
||||||
'height': str(self.device_height),
|
|
||||||
'fill': style['color'],
|
|
||||||
'class': 'device-box',
|
|
||||||
'rx': '5'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Add icon
|
|
||||||
icon_text = SubElement(svg, 'text', {
|
|
||||||
'x': str(x),
|
|
||||||
'y': str(y - 15),
|
|
||||||
'text-anchor': 'middle',
|
|
||||||
'class': 'device-icon'
|
|
||||||
})
|
|
||||||
icon_text.text = style['icon']
|
|
||||||
|
|
||||||
# Add IP address
|
|
||||||
ip_text = SubElement(svg, 'text', {
|
|
||||||
'x': str(x),
|
|
||||||
'y': str(y + 5),
|
|
||||||
'text-anchor': 'middle',
|
|
||||||
'class': 'device-label',
|
|
||||||
'font-weight': 'bold'
|
|
||||||
})
|
|
||||||
ip_text.text = device.get('ip', 'Unknown')
|
|
||||||
|
|
||||||
# Add hostname
|
|
||||||
hostname = device.get('hostname', '')
|
|
||||||
if hostname:
|
|
||||||
hostname_text = SubElement(svg, 'text', {
|
|
||||||
'x': str(x),
|
|
||||||
'y': str(y + 20),
|
|
||||||
'text-anchor': 'middle',
|
|
||||||
'class': 'info-text'
|
|
||||||
})
|
|
||||||
# Truncate long hostnames
|
|
||||||
if len(hostname) > 20:
|
|
||||||
hostname = hostname[:17] + '...'
|
|
||||||
hostname_text.text = hostname
|
|
||||||
|
|
||||||
# Add additional info
|
|
||||||
info_y = y + 32
|
|
||||||
if device.get('ssh_accessible'):
|
|
||||||
ssh_text = SubElement(svg, 'text', {
|
|
||||||
'x': str(x),
|
|
||||||
'y': str(info_y),
|
|
||||||
'text-anchor': 'middle',
|
|
||||||
'class': 'info-text',
|
|
||||||
'fill': 'green'
|
|
||||||
})
|
|
||||||
ssh_text.text = '🔓 SSH'
|
|
||||||
|
|
||||||
def _draw_connections(self, svg: Element, segments: List[Dict], layout: List[List[Tuple]]):
|
|
||||||
"""Draw connections between devices based on routing info"""
|
|
||||||
# This is simplified - you'd analyze routing tables to determine actual connections
|
|
||||||
|
|
||||||
# For now, connect routers between segments
|
|
||||||
routers = []
|
|
||||||
for positions in layout:
|
|
||||||
for x, y, device in positions:
|
|
||||||
if device.get('device_type') in ['router', 'firewall']:
|
|
||||||
routers.append((x, y, device))
|
|
||||||
|
|
||||||
# Connect consecutive routers
|
|
||||||
for i in range(len(routers) - 1):
|
|
||||||
x1, y1, _ = routers[i]
|
|
||||||
x2, y2, _ = routers[i + 1]
|
|
||||||
|
|
||||||
SubElement(svg, 'line', {
|
|
||||||
'x1': str(x1),
|
|
||||||
'y1': str(y1),
|
|
||||||
'x2': str(x2),
|
|
||||||
'y2': str(y2),
|
|
||||||
'class': 'connection'
|
|
||||||
})
|
|
||||||
|
|
||||||
def _add_legend(self, svg: Element):
|
|
||||||
"""Add legend explaining device types"""
|
|
||||||
legend_x = self.width - 200
|
|
||||||
legend_y = self.height - 200
|
|
||||||
|
|
||||||
# Legend box
|
|
||||||
SubElement(svg, 'rect', {
|
|
||||||
'x': str(legend_x - 10),
|
|
||||||
'y': str(legend_y - 10),
|
|
||||||
'width': '190',
|
|
||||||
'height': str(len(self.DEVICE_STYLES) * 25 + 30),
|
|
||||||
'fill': 'white',
|
|
||||||
'stroke': '#333',
|
|
||||||
'stroke-width': '1',
|
|
||||||
'rx': '5'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Legend title
|
|
||||||
title = SubElement(svg, 'text', {
|
|
||||||
'x': str(legend_x),
|
|
||||||
'y': str(legend_y + 5),
|
|
||||||
'class': 'legend-text',
|
|
||||||
'font-weight': 'bold'
|
|
||||||
})
|
|
||||||
title.text = 'Device Types'
|
|
||||||
|
|
||||||
# Legend items
|
|
||||||
y_offset = 25
|
|
||||||
for device_type, style in self.DEVICE_STYLES.items():
|
|
||||||
if device_type == 'default':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Color box
|
|
||||||
SubElement(svg, 'rect', {
|
|
||||||
'x': str(legend_x),
|
|
||||||
'y': str(legend_y + y_offset - 8),
|
|
||||||
'width': '15',
|
|
||||||
'height': '15',
|
|
||||||
'fill': style['color'],
|
|
||||||
'stroke': '#333'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Label
|
|
||||||
label = SubElement(svg, 'text', {
|
|
||||||
'x': str(legend_x + 20),
|
|
||||||
'y': str(legend_y + y_offset + 4),
|
|
||||||
'class': 'legend-text'
|
|
||||||
})
|
|
||||||
label.text = f"{style['icon']} {device_type.replace('_', ' ').title()}"
|
|
||||||
|
|
||||||
y_offset += 25
|
|
||||||
|
|
||||||
def _save_svg(self, svg: Element, output_file: str):
|
|
||||||
"""Save SVG to file with pretty formatting"""
|
|
||||||
rough_string = tostring(svg, encoding='unicode')
|
|
||||||
reparsed = minidom.parseString(rough_string)
|
|
||||||
pretty_svg = reparsed.toprettyxml(indent=' ')
|
|
||||||
|
|
||||||
# Remove extra blank lines
|
|
||||||
pretty_svg = '\n'.join([line for line in pretty_svg.split('\n') if line.strip()])
|
|
||||||
|
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(pretty_svg)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Command line interface"""
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Generate SVG network diagram from scan data')
|
|
||||||
parser.add_argument('input', help='Input JSON file from network scan')
|
|
||||||
parser.add_argument('-o', '--output', default='network_diagram.svg',
|
|
||||||
help='Output SVG file (default: network_diagram.svg)')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Load scan data
|
|
||||||
with open(args.input, 'r') as f:
|
|
||||||
scan_data = json.load(f)
|
|
||||||
|
|
||||||
# Generate diagram
|
|
||||||
generator = NetworkDiagramGenerator(scan_data)
|
|
||||||
generator.generate_svg(args.output)
|
|
||||||
|
|
||||||
print(f"✓ Network diagram generated: {args.output}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -58,7 +58,6 @@ def test_scripts_exist():
|
|||||||
scripts = [
|
scripts = [
|
||||||
'src/network_scanner.py',
|
'src/network_scanner.py',
|
||||||
'src/pfsense_scanner.py',
|
'src/pfsense_scanner.py',
|
||||||
'src/svg_generator.py',
|
|
||||||
'src/integrated_scanner.py',
|
'src/integrated_scanner.py',
|
||||||
'scripts/quickstart.sh'
|
'scripts/quickstart.sh'
|
||||||
]
|
]
|
||||||
@@ -79,7 +78,6 @@ def test_script_syntax():
|
|||||||
scripts = [
|
scripts = [
|
||||||
'src/network_scanner.py',
|
'src/network_scanner.py',
|
||||||
'src/pfsense_scanner.py',
|
'src/pfsense_scanner.py',
|
||||||
'src/svg_generator.py',
|
|
||||||
'src/integrated_scanner.py'
|
'src/integrated_scanner.py'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -230,7 +228,6 @@ def main():
|
|||||||
print("Next steps:")
|
print("Next steps:")
|
||||||
print("1. Edit config.json with your network details")
|
print("1. Edit config.json with your network details")
|
||||||
print("2. Run: ./quickstart.sh")
|
print("2. Run: ./quickstart.sh")
|
||||||
print(" or: ./integrated_scanner.py --generate-svg")
|
|
||||||
else:
|
else:
|
||||||
print("⚠️ Some tests failed. Please check the errors above.")
|
print("⚠️ Some tests failed. Please check the errors above.")
|
||||||
print()
|
print()
|
||||||
|
|||||||
12
test_config.json
Normal file
12
test_config.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"ssh_user": "root",
|
||||||
|
"ssh_key_path": null,
|
||||||
|
"timeout": 2,
|
||||||
|
"additional_networks": [],
|
||||||
|
"special_devices": {},
|
||||||
|
"scan_options": {
|
||||||
|
"max_workers": 10,
|
||||||
|
"ping_timeout": 2,
|
||||||
|
"port_scan_timeout": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
12
tests/test_config.json
Normal file
12
tests/test_config.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"ssh_user": "root",
|
||||||
|
"ssh_key_path": null,
|
||||||
|
"timeout": 2,
|
||||||
|
"additional_networks": [],
|
||||||
|
"special_devices": {},
|
||||||
|
"scan_options": {
|
||||||
|
"max_workers": 10,
|
||||||
|
"ping_timeout": 2,
|
||||||
|
"port_scan_timeout": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1429
tests/test_output.json
Normal file
1429
tests/test_output.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user