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/__init__.py +7 -0
- pynetworkintel/alerts.py +276 -0
- pynetworkintel/analysis/__init__.py +7 -0
- pynetworkintel/analysis/cve.py +184 -0
- pynetworkintel/analysis/llm.py +183 -0
- pynetworkintel/analysis/rules.py +247 -0
- pynetworkintel/changes.py +221 -0
- pynetworkintel/cli.py +353 -0
- pynetworkintel/config.py +191 -0
- pynetworkintel/core.py +170 -0
- pynetworkintel/db.py +218 -0
- pynetworkintel/discovery/__init__.py +6 -0
- pynetworkintel/discovery/scanner.py +243 -0
- pynetworkintel/discovery/ssh_config.py +114 -0
- pynetworkintel/models.py +121 -0
- pynetworkintel/progress.py +169 -0
- pynetworkintel/reporting.py +262 -0
- pynetworkintel/scheduler.py +237 -0
- pynetworkintel-0.2.0.dist-info/METADATA +479 -0
- pynetworkintel-0.2.0.dist-info/RECORD +24 -0
- pynetworkintel-0.2.0.dist-info/WHEEL +5 -0
- pynetworkintel-0.2.0.dist-info/entry_points.txt +2 -0
- pynetworkintel-0.2.0.dist-info/licenses/LICENSE +10 -0
- pynetworkintel-0.2.0.dist-info/top_level.txt +1 -0
pynetworkintel/alerts.py
ADDED
|
@@ -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
|
+
)
|