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.
@@ -0,0 +1,7 @@
1
+ """PyNetworkIntel: AI-powered network intelligence platform."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from pynetworkintel.core import Scanner, Analyzer
6
+
7
+ __all__ = ["Scanner", "Analyzer"]
@@ -0,0 +1,276 @@
1
+ """Alerting system for security findings and changes."""
2
+
3
+ import logging
4
+ import json
5
+ from datetime import datetime
6
+ from typing import List, Dict, Any, Optional, Callable
7
+ from abc import ABC, abstractmethod
8
+ from sqlalchemy.orm import Session
9
+
10
+ from pynetworkintel.db import Database, Alert as DBAlert
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AlertChannel(ABC):
16
+ """Base class for alert channels."""
17
+
18
+ @abstractmethod
19
+ def send(self, alert: Dict[str, Any]) -> bool:
20
+ """Send alert through channel. Returns True if successful."""
21
+ pass
22
+
23
+
24
+ class SlackAlertChannel(AlertChannel):
25
+ """Send alerts to Slack."""
26
+
27
+ def __init__(self, webhook_url: str):
28
+ self.webhook_url = webhook_url
29
+
30
+ def send(self, alert: Dict[str, Any]) -> bool:
31
+ """Send alert to Slack via webhook."""
32
+ try:
33
+ import requests
34
+
35
+ severity_colors = {
36
+ "critical": "#FF0000",
37
+ "high": "#FF6600",
38
+ "medium": "#FFCC00",
39
+ "low": "#00CC00",
40
+ "info": "#0099FF",
41
+ }
42
+
43
+ severity = alert.get("severity", "info").lower()
44
+ color = severity_colors.get(severity, "#999999")
45
+
46
+ payload = {
47
+ "attachments": [
48
+ {
49
+ "color": color,
50
+ "title": alert.get("title", "Security Alert"),
51
+ "text": alert.get("description", ""),
52
+ "fields": [
53
+ {"title": "Severity", "value": severity.upper(), "short": True},
54
+ {"title": "Device", "value": alert.get("device_ip", "unknown"), "short": True},
55
+ {"title": "Type", "value": alert.get("alert_type", "unknown"), "short": True},
56
+ ],
57
+ "footer": "PyNetworkIntel",
58
+ "ts": int(datetime.utcnow().timestamp()),
59
+ }
60
+ ]
61
+ }
62
+
63
+ response = requests.post(self.webhook_url, json=payload, timeout=10)
64
+ return response.status_code == 200
65
+
66
+ except Exception as e:
67
+ logger.error(f"Failed to send Slack alert: {e}")
68
+ return False
69
+
70
+
71
+ class EmailAlertChannel(AlertChannel):
72
+ """Send alerts via email."""
73
+
74
+ def __init__(self, smtp_server: str, smtp_port: int, sender: str, recipients: List[str], password: str = ""):
75
+ self.smtp_server = smtp_server
76
+ self.smtp_port = smtp_port
77
+ self.sender = sender
78
+ self.recipients = recipients
79
+ self.password = password
80
+
81
+ def send(self, alert: Dict[str, Any]) -> bool:
82
+ """Send alert via email."""
83
+ try:
84
+ import smtplib
85
+ from email.mime.text import MIMEText
86
+ from email.mime.multipart import MIMEMultipart
87
+
88
+ severity = alert.get("severity", "INFO").upper()
89
+ subject = f"[{severity}] {alert.get('title', 'Security Alert')}"
90
+
91
+ body = f"""
92
+ PyNetworkIntel Security Alert
93
+
94
+ Type: {alert.get('alert_type', 'Unknown')}
95
+ Severity: {severity}
96
+ Device: {alert.get('device_ip', 'Unknown')}
97
+ Title: {alert.get('title', 'N/A')}
98
+
99
+ Description:
100
+ {alert.get('description', 'No description')}
101
+
102
+ Time: {datetime.utcnow().isoformat()}
103
+ """
104
+
105
+ message = MIMEMultipart()
106
+ message["From"] = self.sender
107
+ message["To"] = ", ".join(self.recipients)
108
+ message["Subject"] = subject
109
+ message.attach(MIMEText(body, "plain"))
110
+
111
+ with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
112
+ if self.password:
113
+ server.starttls()
114
+ server.login(self.sender, self.password)
115
+ server.send_message(message)
116
+
117
+ return True
118
+
119
+ except Exception as e:
120
+ logger.error(f"Failed to send email alert: {e}")
121
+ return False
122
+
123
+
124
+ class WebhookAlertChannel(AlertChannel):
125
+ """Send alerts via generic webhook."""
126
+
127
+ def __init__(self, webhook_url: str):
128
+ self.webhook_url = webhook_url
129
+
130
+ def send(self, alert: Dict[str, Any]) -> bool:
131
+ """Send alert to webhook."""
132
+ try:
133
+ import requests
134
+
135
+ payload = json.dumps(alert, default=str)
136
+ response = requests.post(
137
+ self.webhook_url,
138
+ data=payload,
139
+ headers={"Content-Type": "application/json"},
140
+ timeout=10,
141
+ )
142
+ return response.status_code in (200, 201, 202)
143
+
144
+ except Exception as e:
145
+ logger.error(f"Failed to send webhook alert: {e}")
146
+ return False
147
+
148
+
149
+ class AlertManager:
150
+ """Manage alert channels and routing."""
151
+
152
+ def __init__(self, db: Database):
153
+ self.db = db
154
+ self.channels: Dict[str, AlertChannel] = {}
155
+ self.rules: List[Dict[str, Any]] = []
156
+
157
+ def add_channel(self, name: str, channel: AlertChannel):
158
+ """Add an alert channel."""
159
+ self.channels[name] = channel
160
+ logger.info(f"Registered alert channel: {name}")
161
+
162
+ def add_rule(self, rule: Dict[str, Any]):
163
+ """
164
+ Add alert routing rule.
165
+
166
+ Example:
167
+ {
168
+ "condition": "severity == 'critical'",
169
+ "channels": ["slack", "email"],
170
+ "enabled": True
171
+ }
172
+ """
173
+ self.rules.append(rule)
174
+
175
+ def create_alert(
176
+ self,
177
+ alert_type: str,
178
+ severity: str,
179
+ title: str,
180
+ description: str,
181
+ device_ip: Optional[str] = None,
182
+ cve_id: Optional[str] = None,
183
+ ) -> Optional[Dict[str, Any]]:
184
+ """Create and send alert."""
185
+ alert = {
186
+ "alert_type": alert_type,
187
+ "severity": severity,
188
+ "title": title,
189
+ "description": description,
190
+ "device_ip": device_ip,
191
+ "cve_id": cve_id,
192
+ "created_at": datetime.utcnow().isoformat(),
193
+ }
194
+
195
+ # Save to database
196
+ self._save_alert(alert)
197
+
198
+ # Send through channels
199
+ self._send_alert(alert)
200
+
201
+ return alert
202
+
203
+ def _send_alert(self, alert: Dict[str, Any]):
204
+ """Send alert through appropriate channels."""
205
+ # Determine which channels to use (simple routing based on severity)
206
+ severity_map = {
207
+ "critical": ["slack", "email"],
208
+ "high": ["slack", "email"],
209
+ "medium": ["slack"],
210
+ "low": ["email"],
211
+ "info": [],
212
+ }
213
+
214
+ severity = alert.get("severity", "info").lower()
215
+ channel_names = severity_map.get(severity, [])
216
+
217
+ for channel_name in channel_names:
218
+ if channel_name in self.channels:
219
+ try:
220
+ self.channels[channel_name].send(alert)
221
+ except Exception as e:
222
+ logger.error(f"Error sending alert via {channel_name}: {e}")
223
+
224
+ def _save_alert(self, alert: Dict[str, Any]):
225
+ """Save alert to database."""
226
+ session = self.db.get_session()
227
+
228
+ try:
229
+ db_alert = DBAlert(
230
+ alert_type=alert.get("alert_type"),
231
+ severity=alert.get("severity"),
232
+ title=alert.get("title"),
233
+ description=alert.get("description"),
234
+ device_ip=alert.get("device_ip"),
235
+ cve_id=alert.get("cve_id"),
236
+ status="pending",
237
+ )
238
+ session.add(db_alert)
239
+ session.commit()
240
+
241
+ finally:
242
+ session.close()
243
+
244
+ def get_pending_alerts(self) -> List[Dict[str, Any]]:
245
+ """Get all pending alerts."""
246
+ session = self.db.get_session()
247
+
248
+ try:
249
+ alerts = session.query(DBAlert).filter(DBAlert.status == "pending").all()
250
+ return [
251
+ {
252
+ "id": a.id,
253
+ "alert_type": a.alert_type,
254
+ "severity": a.severity,
255
+ "title": a.title,
256
+ "created_at": a.created_at,
257
+ }
258
+ for a in alerts
259
+ ]
260
+
261
+ finally:
262
+ session.close()
263
+
264
+ def acknowledge_alert(self, alert_id: int):
265
+ """Mark alert as acknowledged."""
266
+ session = self.db.get_session()
267
+
268
+ try:
269
+ alert = session.query(DBAlert).filter(DBAlert.id == alert_id).first()
270
+ if alert:
271
+ alert.acknowledged_at = datetime.utcnow()
272
+ alert.status = "acknowledged"
273
+ session.commit()
274
+
275
+ finally:
276
+ session.close()
@@ -0,0 +1,7 @@
1
+ """Security analysis engines."""
2
+
3
+ from pynetworkintel.analysis.rules import RuleChecker, Rule
4
+ from pynetworkintel.analysis.cve import CVEChecker
5
+ from pynetworkintel.analysis.llm import LLMAnalyzer
6
+
7
+ __all__ = ["RuleChecker", "Rule", "CVEChecker", "LLMAnalyzer"]
@@ -0,0 +1,184 @@
1
+ """CVE database integration and vulnerability checking."""
2
+
3
+ import logging
4
+ import requests
5
+ import time
6
+ from typing import List, Optional, Dict, Any
7
+ from functools import lru_cache
8
+ from datetime import datetime, timedelta
9
+
10
+ from pynetworkintel.models import Device, Service, SecurityFinding, Severity, FindingType
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # NVD API endpoint
15
+ NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
16
+
17
+ # Service name to CVE search keywords
18
+ SERVICE_KEYWORDS = {
19
+ "ssh": "OpenSSH",
20
+ "http": "Apache HTTP",
21
+ "https": "Apache HTTP",
22
+ "apache": "Apache HTTP",
23
+ "nginx": "Nginx",
24
+ "mysql": "MySQL",
25
+ "postgresql": "PostgreSQL",
26
+ "mongodb": "MongoDB",
27
+ "redis": "Redis",
28
+ "docker": "Docker",
29
+ "kubernetes": "Kubernetes",
30
+ "python": "Python",
31
+ "nodejs": "Node.js",
32
+ "java": "Java",
33
+ }
34
+
35
+
36
+ class CVEChecker:
37
+ """Check services for known vulnerabilities via NVD API."""
38
+
39
+ def __init__(self, rate_limit_delay: float = 0.6):
40
+ self.rate_limit_delay = rate_limit_delay
41
+ self.last_request_time = 0.0
42
+ self._cache = {}
43
+
44
+ def check_device(self, device: Device) -> List[SecurityFinding]:
45
+ """
46
+ Check all services on device for known vulnerabilities.
47
+
48
+ Args:
49
+ device: Device to check
50
+
51
+ Returns:
52
+ List of CVE-based security findings
53
+ """
54
+ findings = []
55
+
56
+ for service in device.services:
57
+ if service.version:
58
+ cves = self.get_cves_for_service(service.name, service.version)
59
+ for cve in cves:
60
+ finding = self._create_finding(device, service, cve)
61
+ findings.append(finding)
62
+
63
+ return findings
64
+
65
+ def get_cves_for_service(self, service_name: str, version: str) -> List[Dict[str, Any]]:
66
+ """
67
+ Get CVEs for a specific service and version.
68
+
69
+ Args:
70
+ service_name: Name of the service (e.g., 'Apache', 'OpenSSH')
71
+ version: Version string
72
+
73
+ Returns:
74
+ List of CVE dictionaries
75
+ """
76
+ cache_key = f"{service_name}:{version}"
77
+ if cache_key in self._cache:
78
+ return self._cache[cache_key]
79
+
80
+ keyword = SERVICE_KEYWORDS.get(service_name.lower(), service_name)
81
+ search_query = f"{keyword} {version}"
82
+
83
+ try:
84
+ cves = self._query_nvd(search_query)
85
+ self._cache[cache_key] = cves
86
+ return cves
87
+ except Exception as e:
88
+ logger.warning(f"Failed to query CVEs for {service_name}: {e}")
89
+ return []
90
+
91
+ def _query_nvd(self, keyword: str) -> List[Dict[str, Any]]:
92
+ """Query NVD API for CVEs matching keyword."""
93
+ self._respect_rate_limit()
94
+
95
+ params = {
96
+ "keywordSearch": keyword,
97
+ "resultsPerPage": 5,
98
+ }
99
+
100
+ try:
101
+ response = requests.get(NVD_API_URL, params=params, timeout=10)
102
+ response.raise_for_status()
103
+
104
+ data = response.json()
105
+ vulnerabilities = data.get("vulnerabilities", [])
106
+
107
+ cves = []
108
+ for vuln in vulnerabilities:
109
+ cve_data = vuln.get("cve", {})
110
+ cve_id = cve_data.get("id")
111
+ description = cve_data.get("descriptions", [{}])[0].get("value", "")
112
+ cvss_score = self._extract_cvss_score(cve_data)
113
+
114
+ if cve_id:
115
+ cves.append(
116
+ {
117
+ "id": cve_id,
118
+ "description": description,
119
+ "cvss_score": cvss_score,
120
+ }
121
+ )
122
+
123
+ return cves
124
+
125
+ except requests.RequestException as e:
126
+ logger.error(f"NVD API request failed: {e}")
127
+ return []
128
+
129
+ def _extract_cvss_score(self, cve_data: Dict[str, Any]) -> Optional[float]:
130
+ """Extract CVSS score from CVE data."""
131
+ metrics = cve_data.get("metrics", {})
132
+
133
+ # Try CVSS 3.1 first (more recent)
134
+ cvss31 = metrics.get("cvssMetricV31", [{}])[0]
135
+ if cvss31 and "cvssData" in cvss31:
136
+ return cvss31["cvssData"].get("baseScore")
137
+
138
+ # Fall back to CVSS 2.0
139
+ cvss20 = metrics.get("cvssMetricV20", [{}])[0]
140
+ if cvss20 and "cvssData" in cvss20:
141
+ return cvss20["cvssData"].get("baseScore")
142
+
143
+ return None
144
+
145
+ def _respect_rate_limit(self):
146
+ """Respect NVD API rate limit (1 request per 6 seconds)."""
147
+ elapsed = time.time() - self.last_request_time
148
+ if elapsed < self.rate_limit_delay:
149
+ time.sleep(self.rate_limit_delay - elapsed)
150
+ self.last_request_time = time.time()
151
+
152
+ def _create_finding(
153
+ self, device: Device, service: Service, cve: Dict[str, Any]
154
+ ) -> SecurityFinding:
155
+ """Create security finding from CVE information."""
156
+ severity = self._cvss_to_severity(cve.get("cvss_score"))
157
+
158
+ return SecurityFinding(
159
+ device=f"{device.ip} ({device.hostname or 'unknown'})",
160
+ finding_type=FindingType.CVE,
161
+ severity=severity,
162
+ title=f"{service.name} {service.version} - {cve['id']}",
163
+ description=cve.get("description", "Known vulnerability"),
164
+ evidence=f"Service: {service.name}, Version: {service.version}, Port: {service.port}",
165
+ recommendation=f"Upgrade {service.name} to a patched version",
166
+ business_impact=f"Known vulnerability in {service.name} could allow remote attack",
167
+ cve_id=cve["id"],
168
+ cvss_score=cve.get("cvss_score"),
169
+ )
170
+
171
+ @staticmethod
172
+ def _cvss_to_severity(cvss_score: Optional[float]) -> Severity:
173
+ """Convert CVSS score to severity level."""
174
+ if cvss_score is None:
175
+ return Severity.MEDIUM
176
+
177
+ if cvss_score >= 9.0:
178
+ return Severity.CRITICAL
179
+ elif cvss_score >= 7.0:
180
+ return Severity.HIGH
181
+ elif cvss_score >= 4.0:
182
+ return Severity.MEDIUM
183
+ else:
184
+ return Severity.LOW
@@ -0,0 +1,183 @@
1
+ """LLM-powered analysis and summarization using Claude."""
2
+
3
+ import logging
4
+ import json
5
+ from typing import Optional
6
+ from anthropic import Anthropic
7
+
8
+ from pynetworkintel.models import ScanResult, SecurityFinding, Severity
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class LLMAnalyzer:
14
+ """Use Claude to synthesize findings and provide business-focused analysis."""
15
+
16
+ def __init__(self, api_key: Optional[str] = None):
17
+ self.client = Anthropic(api_key=api_key)
18
+ self.model = "claude-3-5-sonnet-20241022"
19
+
20
+ def summarize_findings(self, scan_result: ScanResult) -> str:
21
+ """
22
+ Synthesize security findings into plain English summary.
23
+
24
+ Args:
25
+ scan_result: Scan results with devices and findings
26
+
27
+ Returns:
28
+ AI-generated plain English summary
29
+ """
30
+ if not scan_result.findings:
31
+ return self._generate_empty_summary(scan_result)
32
+
33
+ prompt = self._build_summary_prompt(scan_result)
34
+
35
+ try:
36
+ message = self.client.messages.create(
37
+ model=self.model,
38
+ max_tokens=1500,
39
+ messages=[{"role": "user", "content": prompt}],
40
+ )
41
+
42
+ return message.content[0].text
43
+
44
+ except Exception as e:
45
+ logger.error(f"LLM summarization failed: {e}")
46
+ return self._generate_fallback_summary(scan_result)
47
+
48
+ def _build_summary_prompt(self, scan_result: ScanResult) -> str:
49
+ """Build prompt for LLM analysis."""
50
+ findings_json = json.dumps(
51
+ [f.to_dict() for f in scan_result.findings],
52
+ indent=2,
53
+ default=str,
54
+ )
55
+
56
+ devices_json = json.dumps(
57
+ [d.to_dict() for d in scan_result.devices],
58
+ indent=2,
59
+ )
60
+
61
+ return f"""You are a network security analyst explaining findings to an electronics/IoT engineer who doesn't understand networking jargon.
62
+
63
+ NETWORK SCAN RESULTS:
64
+ {devices_json}
65
+
66
+ SECURITY FINDINGS:
67
+ {findings_json}
68
+
69
+ Please provide a clear, non-technical summary:
70
+
71
+ 1. **OVERVIEW** (1-2 sentences): Is the network generally healthy or are there problems?
72
+
73
+ 2. **TOP 3 THINGS TO FIX** (in priority order):
74
+ - For each: What is it? Why does it matter? How to fix it? Time required?
75
+
76
+ 3. **WHAT'S GOOD** (if anything):
77
+ - List positive findings
78
+
79
+ 4. **QUESTIONS TO ASK YOUR TEAM**:
80
+ - What questions should they investigate?
81
+
82
+ Keep language simple. Use analogies to hardware/electronics when possible. No networking jargon. Explain WHY each issue matters for business/security/reliability."""
83
+
84
+ def _generate_empty_summary(self, scan_result: ScanResult) -> str:
85
+ """Generate summary when no findings detected."""
86
+ summary = f"""✅ NETWORK HEALTH CHECK - NO ISSUES DETECTED
87
+
88
+ Devices Found: {scan_result.summary()['total_devices']}
89
+ Online: {scan_result.summary()['online_devices']}
90
+ Critical Issues: 0
91
+ High Priority Issues: 0
92
+
93
+ Good news! Your network scan didn't find any major security problems. Everything looks secure and properly configured.
94
+
95
+ However, you should:
96
+ - Continue monitoring regularly (weekly or monthly scans)
97
+ - Keep software updated when new versions are released
98
+ - Review access rules periodically
99
+ """
100
+ return summary
101
+
102
+ def _generate_fallback_summary(self, scan_result: ScanResult) -> str:
103
+ """Fallback summary if LLM fails."""
104
+ summary = scan_result.summary()
105
+ critical = summary["critical_findings"]
106
+ high = summary["high_findings"]
107
+
108
+ output = f"""NETWORK SECURITY SUMMARY
109
+
110
+ Devices Discovered: {summary['total_devices']}
111
+ Online Devices: {summary['online_devices']}
112
+
113
+ ISSUES FOUND:
114
+ - Critical Issues: {critical}
115
+ - High Priority Issues: {high}
116
+ - Total Issues: {summary['total_findings']}
117
+
118
+ TOP ISSUES:
119
+ """
120
+
121
+ # Sort findings by severity
122
+ sorted_findings = sorted(
123
+ scan_result.findings,
124
+ key=lambda f: (
125
+ 0 if f.severity == Severity.CRITICAL
126
+ else 1 if f.severity == Severity.HIGH
127
+ else 2 if f.severity == Severity.MEDIUM
128
+ else 3
129
+ ),
130
+ )
131
+
132
+ for i, finding in enumerate(sorted_findings[:3], 1):
133
+ output += f"""
134
+ {i}. {finding.title}
135
+ Device: {finding.device}
136
+ Why it matters: {finding.business_impact}
137
+ How to fix: {finding.recommendation}
138
+ """
139
+
140
+ return output
141
+
142
+ def explain_finding(self, finding: SecurityFinding) -> str:
143
+ """
144
+ Explain a single finding in plain English.
145
+
146
+ Args:
147
+ finding: Security finding to explain
148
+
149
+ Returns:
150
+ Plain English explanation
151
+ """
152
+ prompt = f"""Explain this security finding to an electronics engineer in simple terms.
153
+ No networking jargon. Use hardware/electronics analogies when helpful.
154
+
155
+ FINDING: {finding.title}
156
+ DEVICE: {finding.device}
157
+ DESCRIPTION: {finding.description}
158
+ EVIDENCE: {finding.evidence}
159
+ IMPACT: {finding.business_impact}
160
+ RECOMMENDATION: {finding.recommendation}
161
+
162
+ Please explain:
163
+ 1. What is this in simple terms?
164
+ 2. Why should they care?
165
+ 3. How to fix it (step by step if possible)?
166
+ 4. How long does it take to fix?"""
167
+
168
+ try:
169
+ message = self.client.messages.create(
170
+ model=self.model,
171
+ max_tokens=800,
172
+ messages=[{"role": "user", "content": prompt}],
173
+ )
174
+
175
+ return message.content[0].text
176
+
177
+ except Exception as e:
178
+ logger.error(f"Explanation generation failed: {e}")
179
+ return (
180
+ f"{finding.title}\n\n"
181
+ f"Why it matters: {finding.business_impact}\n\n"
182
+ f"How to fix: {finding.recommendation}"
183
+ )