pynetworkintel 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pynetworkintel/core.py ADDED
@@ -0,0 +1,170 @@
1
+ """Core scanner and analyzer classes."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Optional, List
6
+ from datetime import datetime
7
+
8
+ from pynetworkintel.models import ScanResult, Device
9
+ from pynetworkintel.discovery import DeviceScanner, SSHConfigGrabber
10
+ from pynetworkintel.analysis import RuleChecker, CVEChecker, LLMAnalyzer
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Configure logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
+ )
19
+
20
+
21
+ class Scanner:
22
+ """High-level device discovery scanner."""
23
+
24
+ def __init__(
25
+ self,
26
+ nmap_path: str = "nmap",
27
+ ssh_username: str = "root",
28
+ ssh_key_path: Optional[str] = None,
29
+ ssh_password: Optional[str] = None,
30
+ ):
31
+ self.device_scanner = DeviceScanner(nmap_path)
32
+ self.ssh_grabber = SSHConfigGrabber(ssh_username, ssh_key_path, ssh_password)
33
+
34
+ def scan(self, target: str, grab_configs: bool = True) -> ScanResult:
35
+ """
36
+ Scan network for devices and gather configuration.
37
+
38
+ Args:
39
+ target: IP address, CIDR range, or hostname
40
+ grab_configs: Whether to attempt SSH config grab (requires credentials)
41
+
42
+ Returns:
43
+ ScanResult with discovered devices
44
+ """
45
+ start_time = time.time()
46
+
47
+ logger.info(f"Starting network scan for {target}")
48
+ devices = self.device_scanner.discover(target)
49
+
50
+ if grab_configs and devices:
51
+ logger.info(f"Attempting to grab configs from {len(devices)} devices")
52
+ for device in devices:
53
+ if self.ssh_grabber.grab_configs(device):
54
+ logger.debug(f"Successfully grabbed configs from {device.ip}")
55
+ else:
56
+ logger.debug(f"Could not grab configs from {device.ip}")
57
+
58
+ duration = time.time() - start_time
59
+
60
+ result = ScanResult(devices=devices, scan_duration_seconds=duration)
61
+
62
+ logger.info(
63
+ f"Scan complete: {len(devices)} devices in {duration:.1f} seconds"
64
+ )
65
+
66
+ return result
67
+
68
+
69
+ class Analyzer:
70
+ """Security analyzer for scan results."""
71
+
72
+ def __init__(self, anthropic_api_key: Optional[str] = None):
73
+ self.rule_checker = RuleChecker()
74
+ self.cve_checker = CVEChecker()
75
+ self.llm_analyzer = LLMAnalyzer(anthropic_api_key)
76
+
77
+ def analyze(self, scan_result: ScanResult) -> ScanResult:
78
+ """
79
+ Perform full security analysis on scan results.
80
+
81
+ Args:
82
+ scan_result: Results from scanner
83
+
84
+ Returns:
85
+ Updated ScanResult with findings
86
+ """
87
+ logger.info("Starting security analysis")
88
+
89
+ for device in scan_result.devices:
90
+ logger.debug(f"Analyzing device {device.ip}")
91
+
92
+ # Check configuration rules
93
+ rule_findings = self.rule_checker.check_device(device)
94
+ scan_result.findings.extend(rule_findings)
95
+
96
+ # Check for known CVEs
97
+ cve_findings = self.cve_checker.check_device(device)
98
+ scan_result.findings.extend(cve_findings)
99
+
100
+ logger.info(f"Analysis complete: {len(scan_result.findings)} findings")
101
+
102
+ return scan_result
103
+
104
+ def summarize(self, scan_result: ScanResult) -> str:
105
+ """
106
+ Generate plain English summary of findings.
107
+
108
+ Args:
109
+ scan_result: Analyzed scan results
110
+
111
+ Returns:
112
+ Plain English summary
113
+ """
114
+ logger.info("Generating AI summary")
115
+ return self.llm_analyzer.summarize_findings(scan_result)
116
+
117
+ def get_summary(self, scan_result: ScanResult) -> dict:
118
+ """Get structured summary of findings."""
119
+ return scan_result.summary()
120
+
121
+
122
+ class Pipeline:
123
+ """Full scanning and analysis pipeline."""
124
+
125
+ def __init__(
126
+ self,
127
+ nmap_path: str = "nmap",
128
+ ssh_username: str = "root",
129
+ ssh_key_path: Optional[str] = None,
130
+ ssh_password: Optional[str] = None,
131
+ anthropic_api_key: Optional[str] = None,
132
+ ):
133
+ self.scanner = Scanner(nmap_path, ssh_username, ssh_key_path, ssh_password)
134
+ self.analyzer = Analyzer(anthropic_api_key)
135
+
136
+ def run(self, target: str, grab_configs: bool = True, summarize: bool = False) -> dict:
137
+ """
138
+ Run complete scan and analysis pipeline.
139
+
140
+ Args:
141
+ target: Network target to scan
142
+ grab_configs: Whether to grab device configs
143
+ summarize: Whether to generate LLM summary
144
+
145
+ Returns:
146
+ Dictionary with results
147
+ """
148
+ logger.info("Starting PyNetworkIntel pipeline")
149
+
150
+ # Scan
151
+ scan_result = self.scanner.scan(target, grab_configs)
152
+
153
+ # Analyze
154
+ scan_result = self.analyzer.analyze(scan_result)
155
+
156
+ result = {
157
+ "devices": [d.to_dict() for d in scan_result.devices],
158
+ "findings": [f.to_dict() for f in scan_result.findings],
159
+ "summary": self.analyzer.get_summary(scan_result),
160
+ "scan_time": scan_result.scan_time.isoformat(),
161
+ "scan_duration_seconds": scan_result.scan_duration_seconds,
162
+ }
163
+
164
+ # Generate summary if requested
165
+ if summarize:
166
+ result["ai_summary"] = self.analyzer.summarize(scan_result)
167
+
168
+ logger.info("Pipeline complete")
169
+
170
+ return result
pynetworkintel/db.py ADDED
@@ -0,0 +1,218 @@
1
+ """Database models and ORM configuration."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional, List
5
+ from sqlalchemy import create_engine, Column, Integer, String, DateTime, Boolean, Float, ForeignKey, Text, JSON, Index
6
+ from sqlalchemy.ext.declarative import declarative_base
7
+ from sqlalchemy.orm import sessionmaker, relationship, Session
8
+ from sqlalchemy.pool import StaticPool
9
+
10
+ Base = declarative_base()
11
+
12
+
13
+ class ScanSession(Base):
14
+ """Represents a network scan execution."""
15
+
16
+ __tablename__ = "scan_sessions"
17
+
18
+ id = Column(Integer, primary_key=True)
19
+ scan_time = Column(DateTime, default=datetime.utcnow, index=True)
20
+ duration_seconds = Column(Float)
21
+ target = Column(String(255))
22
+ device_count = Column(Integer, default=0)
23
+ finding_count = Column(Integer, default=0)
24
+ status = Column(String(50), default="completed") # completed, failed, in_progress
25
+ error_message = Column(Text, nullable=True)
26
+
27
+ devices = relationship("Device", back_populates="scan", cascade="all, delete-orphan")
28
+ findings = relationship("Finding", back_populates="scan", cascade="all, delete-orphan")
29
+
30
+ __table_args__ = (Index("idx_scan_time", "scan_time"), Index("idx_target", "target"))
31
+
32
+
33
+ class Device(Base):
34
+ """Network device discovered during scan."""
35
+
36
+ __tablename__ = "devices"
37
+
38
+ id = Column(Integer, primary_key=True)
39
+ scan_id = Column(Integer, ForeignKey("scan_sessions.id"), index=True)
40
+ ip = Column(String(45), index=True) # IPv4 or IPv6
41
+ hostname = Column(String(255), nullable=True)
42
+ os = Column(String(255), nullable=True)
43
+ device_type = Column(String(50), default="linux")
44
+ is_online = Column(Boolean, default=True)
45
+ last_seen = Column(DateTime, default=datetime.utcnow, index=True)
46
+ first_seen = Column(DateTime, default=datetime.utcnow)
47
+
48
+ scan = relationship("ScanSession", back_populates="devices")
49
+ services = relationship("Service", back_populates="device", cascade="all, delete-orphan")
50
+ configs = relationship("DeviceConfig", back_populates="device", cascade="all, delete-orphan")
51
+
52
+ __table_args__ = (Index("idx_device_ip_scan", "ip", "scan_id"),)
53
+
54
+
55
+ class Service(Base):
56
+ """Network service running on a device."""
57
+
58
+ __tablename__ = "services"
59
+
60
+ id = Column(Integer, primary_key=True)
61
+ device_id = Column(Integer, ForeignKey("devices.id"), index=True)
62
+ port = Column(Integer)
63
+ name = Column(String(100))
64
+ version = Column(String(255), nullable=True)
65
+ protocol = Column(String(10), default="tcp")
66
+ state = Column(String(50), default="open")
67
+ first_seen = Column(DateTime, default=datetime.utcnow)
68
+ last_seen = Column(DateTime, default=datetime.utcnow)
69
+
70
+ device = relationship("Device", back_populates="services")
71
+
72
+ __table_args__ = (Index("idx_service_device_port", "device_id", "port"),)
73
+
74
+
75
+ class DeviceConfig(Base):
76
+ """Configuration file content from device."""
77
+
78
+ __tablename__ = "device_configs"
79
+
80
+ id = Column(Integer, primary_key=True)
81
+ device_id = Column(Integer, ForeignKey("devices.id"), index=True)
82
+ path = Column(String(500))
83
+ content = Column(Text)
84
+ device_type = Column(String(50))
85
+ captured_at = Column(DateTime, default=datetime.utcnow)
86
+
87
+ device = relationship("Device", back_populates="configs")
88
+
89
+
90
+ class Finding(Base):
91
+ """Security finding from analysis."""
92
+
93
+ __tablename__ = "findings"
94
+
95
+ id = Column(Integer, primary_key=True)
96
+ scan_id = Column(Integer, ForeignKey("scan_sessions.id"), index=True)
97
+ device_ip = Column(String(45), index=True)
98
+ device_hostname = Column(String(255), nullable=True)
99
+ finding_type = Column(String(50)) # configuration, cve, exposure, performance
100
+ severity = Column(String(20), index=True) # critical, high, medium, low, info
101
+ title = Column(String(500))
102
+ description = Column(Text)
103
+ evidence = Column(Text)
104
+ recommendation = Column(Text)
105
+ business_impact = Column(Text)
106
+ cve_id = Column(String(50), nullable=True, index=True)
107
+ cvss_score = Column(Float, nullable=True)
108
+ exploit_available = Column(Boolean, default=False)
109
+ found_at = Column(DateTime, default=datetime.utcnow, index=True)
110
+ resolved_at = Column(DateTime, nullable=True)
111
+
112
+ scan = relationship("ScanSession", back_populates="findings")
113
+
114
+ __table_args__ = (
115
+ Index("idx_finding_severity_scan", "severity", "scan_id"),
116
+ Index("idx_finding_device_ip", "device_ip"),
117
+ )
118
+
119
+
120
+ class VulnerabilityChange(Base):
121
+ """Track when vulnerabilities appear/disappear."""
122
+
123
+ __tablename__ = "vulnerability_changes"
124
+
125
+ id = Column(Integer, primary_key=True)
126
+ device_ip = Column(String(45), index=True)
127
+ cve_id = Column(String(50), index=True)
128
+ title = Column(String(500))
129
+ appeared_at = Column(DateTime, default=datetime.utcnow, index=True)
130
+ resolved_at = Column(DateTime, nullable=True)
131
+ severity = Column(String(20))
132
+
133
+ __table_args__ = (Index("idx_vuln_device_cve", "device_ip", "cve_id"),)
134
+
135
+
136
+ class DeviceChange(Base):
137
+ """Track device inventory changes."""
138
+
139
+ __tablename__ = "device_changes"
140
+
141
+ id = Column(Integer, primary_key=True)
142
+ ip = Column(String(45), index=True)
143
+ hostname = Column(String(255), nullable=True)
144
+ change_type = Column(String(50)) # discovered, removed, online, offline
145
+ detected_at = Column(DateTime, default=datetime.utcnow, index=True)
146
+ details = Column(JSON, nullable=True)
147
+
148
+ __table_args__ = (Index("idx_device_change_ip", "ip"),)
149
+
150
+
151
+ class Alert(Base):
152
+ """Security alert/notification."""
153
+
154
+ __tablename__ = "alerts"
155
+
156
+ id = Column(Integer, primary_key=True)
157
+ alert_type = Column(String(50)) # new_vulnerability, device_offline, config_change
158
+ severity = Column(String(20))
159
+ title = Column(String(500))
160
+ description = Column(Text)
161
+ device_ip = Column(String(45), nullable=True)
162
+ cve_id = Column(String(50), nullable=True)
163
+ created_at = Column(DateTime, default=datetime.utcnow, index=True)
164
+ sent_at = Column(DateTime, nullable=True)
165
+ acknowledged_at = Column(DateTime, nullable=True)
166
+ status = Column(String(50), default="pending") # pending, sent, acknowledged, resolved
167
+
168
+ __table_args__ = (
169
+ Index("idx_alert_status", "status"),
170
+ Index("idx_alert_created", "created_at"),
171
+ )
172
+
173
+
174
+ class Database:
175
+ """Database connection and session management."""
176
+
177
+ def __init__(self, database_url: str = "sqlite:///pynetworkintel.db"):
178
+ self.database_url = database_url
179
+ self.engine = create_engine(
180
+ database_url,
181
+ echo=False,
182
+ # Use StaticPool for SQLite to avoid threading issues
183
+ poolclass=StaticPool if database_url.startswith("sqlite") else None,
184
+ )
185
+ self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
186
+
187
+ def create_all(self):
188
+ """Create all database tables."""
189
+ Base.metadata.create_all(bind=self.engine)
190
+
191
+ def get_session(self) -> Session:
192
+ """Get a new database session."""
193
+ return self.SessionLocal()
194
+
195
+ def close(self):
196
+ """Close all connections."""
197
+ self.engine.dispose()
198
+
199
+
200
+ # Singleton database instance
201
+ _db_instance: Optional[Database] = None
202
+
203
+
204
+ def init_db(database_url: str = "sqlite:///pynetworkintel.db") -> Database:
205
+ """Initialize database singleton."""
206
+ global _db_instance
207
+ _db_instance = Database(database_url)
208
+ _db_instance.create_all()
209
+ return _db_instance
210
+
211
+
212
+ def get_db() -> Database:
213
+ """Get singleton database instance."""
214
+ global _db_instance
215
+ if _db_instance is None:
216
+ _db_instance = Database()
217
+ _db_instance.create_all()
218
+ return _db_instance
@@ -0,0 +1,6 @@
1
+ """Device discovery engines."""
2
+
3
+ from pynetworkintel.discovery.scanner import DeviceScanner, NmapScanner, ARPScanner
4
+ from pynetworkintel.discovery.ssh_config import SSHConfigGrabber
5
+
6
+ __all__ = ["DeviceScanner", "NmapScanner", "ARPScanner", "SSHConfigGrabber"]
@@ -0,0 +1,243 @@
1
+ """Network device discovery engine."""
2
+
3
+ import subprocess
4
+ import json
5
+ import logging
6
+ import socket
7
+ from typing import List, Optional, Set
8
+ from datetime import datetime
9
+ import xml.etree.ElementTree as ET
10
+
11
+ from pynetworkintel.models import Device, Service
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class NmapScanner:
17
+ """Wrapper around nmap for service discovery."""
18
+
19
+ def __init__(self, nmap_path: str = "nmap"):
20
+ self.nmap_path = nmap_path
21
+ self._check_nmap_available()
22
+
23
+ def _check_nmap_available(self):
24
+ try:
25
+ subprocess.run([self.nmap_path, "-V"], capture_output=True, timeout=5)
26
+ except (subprocess.TimeoutExpired, FileNotFoundError):
27
+ logger.warning(f"nmap not found at {self.nmap_path}. Some discovery features will be limited.")
28
+
29
+ def scan(self, target: str) -> List[Device]:
30
+ """
31
+ Scan target subnet/host for active devices and services.
32
+
33
+ Args:
34
+ target: IP address, CIDR range (10.0.0.0/24), or hostname
35
+
36
+ Returns:
37
+ List of discovered devices with service information
38
+ """
39
+ devices = []
40
+
41
+ try:
42
+ cmd = [
43
+ self.nmap_path,
44
+ "-sV",
45
+ "--script=smb-os-discovery",
46
+ "-oX",
47
+ "-",
48
+ target,
49
+ ]
50
+
51
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
52
+
53
+ if result.returncode not in (0, 1):
54
+ logger.error(f"nmap scan failed: {result.stderr}")
55
+ return devices
56
+
57
+ devices = self._parse_nmap_xml(result.stdout)
58
+
59
+ except subprocess.TimeoutExpired:
60
+ logger.error(f"nmap scan timeout for {target}")
61
+ except Exception as e:
62
+ logger.error(f"nmap scan error: {e}")
63
+
64
+ return devices
65
+
66
+ def _parse_nmap_xml(self, xml_output: str) -> List[Device]:
67
+ """Parse nmap XML output and extract device information."""
68
+ devices = []
69
+
70
+ try:
71
+ root = ET.fromstring(xml_output)
72
+ except ET.ParseError:
73
+ logger.error("Failed to parse nmap XML output")
74
+ return devices
75
+
76
+ for host in root.findall(".//host"):
77
+ status = host.find("status")
78
+ if status is None or status.get("state") != "up":
79
+ continue
80
+
81
+ device = self._parse_host(host)
82
+ if device:
83
+ devices.append(device)
84
+
85
+ return devices
86
+
87
+ def _parse_host(self, host_elem: ET.Element) -> Optional[Device]:
88
+ """Parse individual host element from nmap XML."""
89
+ address_elem = host_elem.find("address")
90
+ if address_elem is None:
91
+ return None
92
+
93
+ ip = address_elem.get("addr")
94
+ if not ip:
95
+ return None
96
+
97
+ device = Device(ip=ip, is_online=True)
98
+
99
+ # Get OS information
100
+ os_elem = host_elem.find(".//os/osmatch")
101
+ if os_elem is not None:
102
+ device.os = os_elem.get("name")
103
+
104
+ # Get hostname
105
+ hostnames = host_elem.findall(".//hostname")
106
+ if hostnames:
107
+ device.hostname = hostnames[0].get("name")
108
+
109
+ # Parse services
110
+ ports = host_elem.findall(".//port")
111
+ for port_elem in ports:
112
+ service_data = self._parse_service(port_elem)
113
+ if service_data:
114
+ device.add_service(**service_data)
115
+
116
+ return device
117
+
118
+ def _parse_service(self, port_elem: ET.Element) -> Optional[dict]:
119
+ """Parse service information from port element."""
120
+ state = port_elem.find("state")
121
+ if state is None or state.get("state") != "open":
122
+ return None
123
+
124
+ port = int(port_elem.get("portid"))
125
+ protocol = port_elem.get("protocol", "tcp")
126
+
127
+ service_elem = port_elem.find("service")
128
+ service_name = "unknown"
129
+ service_version = None
130
+
131
+ if service_elem is not None:
132
+ service_name = service_elem.get("name", "unknown")
133
+ service_version = service_elem.get("version")
134
+
135
+ return {
136
+ "port": port,
137
+ "name": service_name,
138
+ "version": service_version,
139
+ "protocol": protocol,
140
+ }
141
+
142
+
143
+ class ARPScanner:
144
+ """Simple ARP-based device discovery for local networks."""
145
+
146
+ def scan_local(self, interface: Optional[str] = None) -> List[str]:
147
+ """
148
+ Discover active devices on local network via ARP.
149
+
150
+ Args:
151
+ interface: Network interface to scan (e.g., 'eth0', 'en0'). Auto-detect if None.
152
+
153
+ Returns:
154
+ List of discovered IP addresses
155
+ """
156
+ ips = []
157
+
158
+ try:
159
+ if interface is None:
160
+ interface = self._get_default_interface()
161
+
162
+ cmd = ["arp-scan", "-l", "-I", interface]
163
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
164
+
165
+ for line in result.stdout.split("\n"):
166
+ if "\t" in line:
167
+ parts = line.split("\t")
168
+ if len(parts) > 0:
169
+ ip = parts[0].strip()
170
+ if self._is_valid_ip(ip):
171
+ ips.append(ip)
172
+
173
+ except (subprocess.TimeoutExpired, FileNotFoundError):
174
+ logger.warning("arp-scan not available, using nmap ping discovery instead")
175
+
176
+ return ips
177
+
178
+ def _get_default_interface(self) -> str:
179
+ """Get default network interface."""
180
+ try:
181
+ result = subprocess.run(
182
+ ["ip", "route", "show"],
183
+ capture_output=True,
184
+ text=True,
185
+ timeout=5,
186
+ )
187
+ for line in result.stdout.split("\n"):
188
+ if "default" in line:
189
+ parts = line.split()
190
+ if len(parts) > 4:
191
+ return parts[4]
192
+ except Exception:
193
+ pass
194
+
195
+ return "eth0"
196
+
197
+ @staticmethod
198
+ def _is_valid_ip(ip: str) -> bool:
199
+ try:
200
+ socket.inet_aton(ip)
201
+ return True
202
+ except socket.error:
203
+ return False
204
+
205
+
206
+ class DeviceScanner:
207
+ """High-level device discovery orchestrator."""
208
+
209
+ def __init__(self, nmap_path: str = "nmap"):
210
+ self.nmap_scanner = NmapScanner(nmap_path)
211
+ self.arp_scanner = ARPScanner()
212
+
213
+ def discover(self, target: str) -> List[Device]:
214
+ """
215
+ Discover devices in target network.
216
+
217
+ Args:
218
+ target: IP address, CIDR range, or hostname
219
+
220
+ Returns:
221
+ List of discovered devices
222
+ """
223
+ logger.info(f"Starting device discovery for {target}")
224
+
225
+ devices = self.nmap_scanner.scan(target)
226
+
227
+ logger.info(f"Discovered {len(devices)} devices")
228
+
229
+ return devices
230
+
231
+ def discover_local(self) -> List[Device]:
232
+ """Discover devices on local network."""
233
+ logger.info("Starting local network discovery via ARP")
234
+
235
+ ips = self.arp_scanner.scan_local()
236
+ logger.info(f"Found {len(ips)} local devices via ARP")
237
+
238
+ devices = []
239
+ for ip in ips:
240
+ device = Device(ip=ip, is_online=True)
241
+ devices.append(device)
242
+
243
+ return devices