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/cli.py ADDED
@@ -0,0 +1,353 @@
1
+ """Command-line interface for PyNetworkIntel."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ import logging
7
+ import os
8
+ from typing import Optional
9
+
10
+ from pynetworkintel import Scanner, Analyzer
11
+ from pynetworkintel.core import Pipeline
12
+ from pynetworkintel.config import ConfigManager
13
+ from pynetworkintel.progress import ProgressIndicator, OutputFormatter
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Progress indicator (global)
18
+ progress = ProgressIndicator()
19
+ formatter = OutputFormatter()
20
+
21
+
22
+ def main():
23
+ """Main CLI entry point."""
24
+ parser = argparse.ArgumentParser(
25
+ description="AI-powered network intelligence and vulnerability discovery",
26
+ formatter_class=argparse.RawDescriptionHelpFormatter,
27
+ epilog="""
28
+ Examples:
29
+ # Scan a subnet
30
+ pynetworkintel scan 192.168.1.0/24
31
+
32
+ # Scan with config grabbing (requires SSH access)
33
+ pynetworkintel scan 192.168.1.0/24 --ssh-user admin --ssh-key ~/.ssh/id_rsa
34
+
35
+ # Run full pipeline with LLM summary
36
+ pynetworkintel analyze 192.168.1.0/24 --summarize
37
+
38
+ # Get findings in JSON format
39
+ pynetworkintel scan 192.168.1.0/24 --output json
40
+
41
+ # Save results to file
42
+ pynetworkintel analyze 192.168.1.0/24 --output-file results.json
43
+
44
+ # Use custom config file
45
+ pynetworkintel scan 192.168.1.0/24 --config ~/.pynetworkintel/config.yaml
46
+
47
+ # Initialize default config
48
+ pynetworkintel init-config
49
+ """,
50
+ )
51
+
52
+ # Global options
53
+ parser.add_argument(
54
+ "--version",
55
+ action="version",
56
+ version="%(prog)s 0.2.0",
57
+ )
58
+ parser.add_argument(
59
+ "--verbose",
60
+ action="store_true",
61
+ help="Enable verbose logging",
62
+ )
63
+ parser.add_argument(
64
+ "--debug",
65
+ action="store_true",
66
+ help="Enable debug logging",
67
+ )
68
+ parser.add_argument(
69
+ "--config",
70
+ help="Path to config file (default: ~/.pynetworkintel/config.yaml)",
71
+ )
72
+ parser.add_argument(
73
+ "--no-color",
74
+ action="store_true",
75
+ help="Disable colored output",
76
+ )
77
+
78
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
79
+
80
+ # Init config command
81
+ init_parser = subparsers.add_parser("init-config", help="Initialize default config")
82
+ init_parser.set_defaults(func=handle_init_config)
83
+
84
+ # Scan command
85
+ scan_parser = subparsers.add_parser("scan", help="Scan network for devices")
86
+ scan_parser.add_argument("target", help="Target IP, CIDR range, or hostname")
87
+ scan_parser.add_argument(
88
+ "--no-config-grab",
89
+ action="store_true",
90
+ help="Skip SSH config grabbing",
91
+ )
92
+ scan_parser.add_argument(
93
+ "--ssh-user",
94
+ default="root",
95
+ help="SSH username for config grabbing (default: root)",
96
+ )
97
+ scan_parser.add_argument(
98
+ "--ssh-key",
99
+ help="SSH private key path for authentication",
100
+ )
101
+ scan_parser.add_argument(
102
+ "--ssh-password",
103
+ help="SSH password (if not using key-based auth)",
104
+ )
105
+ scan_parser.add_argument(
106
+ "--output",
107
+ choices=["text", "json"],
108
+ default="text",
109
+ help="Output format",
110
+ )
111
+ scan_parser.add_argument(
112
+ "--output-file",
113
+ help="Save results to file (auto-detect format from extension)",
114
+ )
115
+ scan_parser.set_defaults(func=handle_scan)
116
+
117
+ # Analyze command
118
+ analyze_parser = subparsers.add_parser(
119
+ "analyze", help="Scan and perform full analysis"
120
+ )
121
+ analyze_parser.add_argument("target", help="Target IP, CIDR range, or hostname")
122
+ analyze_parser.add_argument(
123
+ "--ssh-user",
124
+ default="root",
125
+ help="SSH username (default: root)",
126
+ )
127
+ analyze_parser.add_argument(
128
+ "--ssh-key",
129
+ help="SSH private key path",
130
+ )
131
+ analyze_parser.add_argument(
132
+ "--ssh-password",
133
+ help="SSH password",
134
+ )
135
+ analyze_parser.add_argument(
136
+ "--summarize",
137
+ action="store_true",
138
+ help="Generate AI-powered summary of findings",
139
+ )
140
+ analyze_parser.add_argument(
141
+ "--output",
142
+ choices=["text", "json"],
143
+ default="text",
144
+ help="Output format",
145
+ )
146
+ analyze_parser.add_argument(
147
+ "--api-key",
148
+ help="Anthropic API key for AI summaries (or set ANTHROPIC_API_KEY env var)",
149
+ )
150
+ analyze_parser.add_argument(
151
+ "--output-file",
152
+ help="Save results to file",
153
+ )
154
+ analyze_parser.set_defaults(func=handle_analyze)
155
+
156
+ args = parser.parse_args()
157
+
158
+ # Setup logging
159
+ if args.debug:
160
+ logging.basicConfig(level=logging.DEBUG)
161
+ elif args.verbose:
162
+ logging.basicConfig(level=logging.INFO)
163
+ else:
164
+ logging.basicConfig(level=logging.WARNING)
165
+
166
+ if not args.command:
167
+ parser.print_help()
168
+ return 1
169
+
170
+ try:
171
+ return args.func(args)
172
+ except KeyboardInterrupt:
173
+ progress.error("Scanning interrupted by user")
174
+ return 1
175
+ except Exception as e:
176
+ progress.error(f"{e}")
177
+ logger.exception("Unhandled error")
178
+ return 1
179
+
180
+
181
+ def handle_init_config(args) -> int:
182
+ """Handle init-config command."""
183
+ config_manager = ConfigManager()
184
+ config_manager.create_default_config()
185
+ progress.success(f"Config created at {config_manager.config_file}")
186
+ return 0
187
+
188
+
189
+ def handle_scan(args) -> int:
190
+ """Handle scan command."""
191
+ progress.section(f"Scanning {args.target}")
192
+
193
+ scanner = Scanner(
194
+ ssh_username=args.ssh_user,
195
+ ssh_key_path=args.ssh_key,
196
+ ssh_password=args.ssh_password,
197
+ )
198
+
199
+ progress.status(f"Starting scan of {args.target}...")
200
+
201
+ scan_result = scanner.scan(
202
+ args.target,
203
+ grab_configs=not args.no_config_grab,
204
+ )
205
+
206
+ progress.success(f"Discovered {len(scan_result.devices)} devices in {scan_result.scan_duration_seconds:.1f}s")
207
+
208
+ if args.output == "json":
209
+ output = scan_result.to_dict()
210
+ result_str = json.dumps(output, indent=2, default=str)
211
+ else:
212
+ result_str = format_scan_output(scan_result)
213
+
214
+ if args.output_file:
215
+ with open(args.output_file, "w") as f:
216
+ f.write(result_str)
217
+ progress.success(f"Results saved to {args.output_file}")
218
+ else:
219
+ print(result_str)
220
+
221
+ return 0
222
+
223
+
224
+ def handle_analyze(args) -> int:
225
+ """Handle analyze command."""
226
+ api_key = getattr(args, 'api_key', None) or os.getenv("ANTHROPIC_API_KEY")
227
+
228
+ progress.section(f"Analyzing {args.target}")
229
+
230
+ pipeline = Pipeline(
231
+ ssh_username=args.ssh_user,
232
+ ssh_key_path=args.ssh_key,
233
+ ssh_password=args.ssh_password,
234
+ anthropic_api_key=api_key,
235
+ )
236
+
237
+ progress.status(f"Scanning network {args.target}...")
238
+
239
+ result = pipeline.run(
240
+ args.target,
241
+ grab_configs=True,
242
+ summarize=args.summarize,
243
+ )
244
+
245
+ if args.output == "json":
246
+ result_str = json.dumps(result, indent=2, default=str)
247
+ else:
248
+ result_str = format_analysis_output(result, args.summarize)
249
+
250
+ if args.output_file:
251
+ with open(args.output_file, "w") as f:
252
+ f.write(result_str)
253
+ progress.success(f"Results saved to {args.output_file}")
254
+ else:
255
+ print(result_str)
256
+
257
+ return 0
258
+
259
+
260
+ def format_scan_output(scan_result) -> str:
261
+ """Format scan results in human-readable format."""
262
+ output = []
263
+ output.append("="*60)
264
+ output.append("NETWORK SCAN RESULTS")
265
+ output.append("="*60)
266
+ output.append("")
267
+
268
+ output.append(f"Devices Found: {len(scan_result.devices)}")
269
+ output.append(f"Online Devices: {sum(1 for d in scan_result.devices if d.is_online)}")
270
+ output.append(f"Scan Duration: {scan_result.scan_duration_seconds:.1f}s")
271
+ output.append("")
272
+
273
+ if scan_result.devices:
274
+ output.append("Discovered Devices:")
275
+ output.append("-" * 60)
276
+ for device in scan_result.devices:
277
+ status = "✓ ONLINE" if device.is_online else "✗ OFFLINE"
278
+ hostname = device.hostname or "unknown"
279
+ os_info = device.os or "unknown"
280
+
281
+ output.append(f"\n{device.ip} ({hostname})")
282
+ output.append(f" Status: {status}")
283
+ output.append(f" OS: {os_info}")
284
+
285
+ if device.services:
286
+ output.append(f" Services:")
287
+ for service in device.services:
288
+ version = f" {service.version}" if service.version else ""
289
+ output.append(f" - Port {service.port}: {service.name}{version}")
290
+
291
+ if device.configs:
292
+ output.append(f" Configs: {len(device.configs)} files grabbed")
293
+
294
+ return "\n".join(output)
295
+
296
+
297
+ def format_analysis_output(result: dict, show_summary: bool = False) -> str:
298
+ """Format analysis results in human-readable format."""
299
+ output = []
300
+ output.append("="*60)
301
+ output.append("NETWORK ANALYSIS RESULTS")
302
+ output.append("="*60)
303
+ output.append("")
304
+
305
+ summary = result.get("summary", {})
306
+ output.append(f"Devices Found: {summary.get('total_devices', 0)}")
307
+ output.append(f"Online: {summary.get('online_devices', 0)}")
308
+ output.append(f"Critical Findings: {summary.get('critical_findings', 0)}")
309
+ output.append(f"High Priority Findings: {summary.get('high_findings', 0)}")
310
+ output.append(f"Total Issues: {summary.get('total_findings', 0)}")
311
+ output.append("")
312
+
313
+ findings = result.get("findings", [])
314
+ if findings:
315
+ output.append("Security Findings (ordered by severity):")
316
+ output.append("-" * 60)
317
+
318
+ # Sort by severity
319
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
320
+ sorted_findings = sorted(
321
+ findings, key=lambda f: severity_order.get(f.get("severity"), 5)
322
+ )
323
+
324
+ for i, finding in enumerate(sorted_findings[:10], 1):
325
+ severity = finding.get("severity", "unknown").upper()
326
+ title = finding.get("title", "Unknown")
327
+ device = finding.get("device", "unknown")
328
+ impact = finding.get("business_impact", "")
329
+ rec = finding.get("recommendation", "")
330
+
331
+ output.append(f"\n{i}. [{severity}] {title}")
332
+ output.append(f" Device: {device}")
333
+ output.append(f" Impact: {impact}")
334
+ output.append(f" Fix: {rec}")
335
+
336
+ if len(findings) > 10:
337
+ output.append(f"\n... and {len(findings) - 10} more findings")
338
+ else:
339
+ output.append("✓ No security issues found!")
340
+
341
+ if show_summary and "ai_summary" in result:
342
+ output.append("")
343
+ output.append("="*60)
344
+ output.append("AI ANALYSIS SUMMARY")
345
+ output.append("="*60)
346
+ output.append("")
347
+ output.append(result["ai_summary"])
348
+
349
+ return "\n".join(output)
350
+
351
+
352
+ if __name__ == "__main__":
353
+ sys.exit(main())
@@ -0,0 +1,191 @@
1
+ """Configuration management for PyNetworkIntel."""
2
+
3
+ import os
4
+ import yaml
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Dict, Any, Optional
8
+ from dataclasses import dataclass, field
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ CONFIG_DIR = Path.home() / ".pynetworkintel"
13
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
14
+
15
+
16
+ @dataclass
17
+ class SSHConfig:
18
+ username: str = "root"
19
+ key_path: Optional[str] = None
20
+ password: Optional[str] = None
21
+ timeout: int = 10
22
+
23
+ def to_dict(self) -> Dict[str, Any]:
24
+ return {
25
+ "username": self.username,
26
+ "key_path": self.key_path,
27
+ "password": self.password,
28
+ "timeout": self.timeout,
29
+ }
30
+
31
+
32
+ @dataclass
33
+ class ScanConfig:
34
+ timeout: int = 300
35
+ nmap_args: str = "-sV"
36
+ grab_configs: bool = True
37
+ retry_count: int = 1
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ return {
41
+ "timeout": self.timeout,
42
+ "nmap_args": self.nmap_args,
43
+ "grab_configs": self.grab_configs,
44
+ "retry_count": self.retry_count,
45
+ }
46
+
47
+
48
+ @dataclass
49
+ class AnalysisConfig:
50
+ enable_cve_check: bool = True
51
+ enable_llm_summary: bool = True
52
+ cve_cache_ttl: int = 86400 # 1 day
53
+ llm_model: str = "claude-3-5-sonnet-20241022"
54
+
55
+ def to_dict(self) -> Dict[str, Any]:
56
+ return {
57
+ "enable_cve_check": self.enable_cve_check,
58
+ "enable_llm_summary": self.enable_llm_summary,
59
+ "cve_cache_ttl": self.cve_cache_ttl,
60
+ "llm_model": self.llm_model,
61
+ }
62
+
63
+
64
+ @dataclass
65
+ class OutputConfig:
66
+ format: str = "text" # text, json, csv
67
+ verbose: bool = False
68
+ color: bool = True
69
+ output_file: Optional[str] = None
70
+
71
+ def to_dict(self) -> Dict[str, Any]:
72
+ return {
73
+ "format": self.format,
74
+ "verbose": self.verbose,
75
+ "color": self.color,
76
+ "output_file": self.output_file,
77
+ }
78
+
79
+
80
+ @dataclass
81
+ class AppConfig:
82
+ ssh: SSHConfig = field(default_factory=SSHConfig)
83
+ scan: ScanConfig = field(default_factory=ScanConfig)
84
+ analysis: AnalysisConfig = field(default_factory=AnalysisConfig)
85
+ output: OutputConfig = field(default_factory=OutputConfig)
86
+
87
+ def to_dict(self) -> Dict[str, Any]:
88
+ return {
89
+ "ssh": self.ssh.to_dict(),
90
+ "scan": self.scan.to_dict(),
91
+ "analysis": self.analysis.to_dict(),
92
+ "output": self.output.to_dict(),
93
+ }
94
+
95
+
96
+ class ConfigManager:
97
+ """Manage PyNetworkIntel configuration."""
98
+
99
+ def __init__(self):
100
+ self.config_dir = CONFIG_DIR
101
+ self.config_file = CONFIG_FILE
102
+ self.config = self._load_config()
103
+
104
+ def _load_config(self) -> AppConfig:
105
+ """Load configuration from file or use defaults."""
106
+ if self.config_file.exists():
107
+ try:
108
+ with open(self.config_file, "r") as f:
109
+ data = yaml.safe_load(f) or {}
110
+ return self._parse_config(data)
111
+ except Exception as e:
112
+ logger.warning(f"Failed to load config file: {e}, using defaults")
113
+
114
+ return AppConfig()
115
+
116
+ def _parse_config(self, data: Dict[str, Any]) -> AppConfig:
117
+ """Parse YAML config data into AppConfig."""
118
+ config = AppConfig()
119
+
120
+ if "ssh" in data:
121
+ ssh_data = data["ssh"]
122
+ config.ssh = SSHConfig(
123
+ username=ssh_data.get("username", config.ssh.username),
124
+ key_path=ssh_data.get("key_path"),
125
+ password=ssh_data.get("password"),
126
+ timeout=ssh_data.get("timeout", config.ssh.timeout),
127
+ )
128
+
129
+ if "scan" in data:
130
+ scan_data = data["scan"]
131
+ config.scan = ScanConfig(
132
+ timeout=scan_data.get("timeout", config.scan.timeout),
133
+ nmap_args=scan_data.get("nmap_args", config.scan.nmap_args),
134
+ grab_configs=scan_data.get("grab_configs", config.scan.grab_configs),
135
+ retry_count=scan_data.get("retry_count", config.scan.retry_count),
136
+ )
137
+
138
+ if "analysis" in data:
139
+ analysis_data = data["analysis"]
140
+ config.analysis = AnalysisConfig(
141
+ enable_cve_check=analysis_data.get("enable_cve_check", config.analysis.enable_cve_check),
142
+ enable_llm_summary=analysis_data.get("enable_llm_summary", config.analysis.enable_llm_summary),
143
+ cve_cache_ttl=analysis_data.get("cve_cache_ttl", config.analysis.cve_cache_ttl),
144
+ llm_model=analysis_data.get("llm_model", config.analysis.llm_model),
145
+ )
146
+
147
+ if "output" in data:
148
+ output_data = data["output"]
149
+ config.output = OutputConfig(
150
+ format=output_data.get("format", config.output.format),
151
+ verbose=output_data.get("verbose", config.output.verbose),
152
+ color=output_data.get("color", config.output.color),
153
+ output_file=output_data.get("output_file"),
154
+ )
155
+
156
+ return config
157
+
158
+ def save_config(self, config: AppConfig):
159
+ """Save configuration to file."""
160
+ self.config_dir.mkdir(parents=True, exist_ok=True)
161
+
162
+ with open(self.config_file, "w") as f:
163
+ yaml.dump(config.to_dict(), f, default_flow_style=False)
164
+
165
+ logger.info(f"Config saved to {self.config_file}")
166
+
167
+ def create_default_config(self):
168
+ """Create default configuration file."""
169
+ config = AppConfig()
170
+ self.save_config(config)
171
+ logger.info(f"Created default config at {self.config_file}")
172
+
173
+ def update_from_env(self):
174
+ """Update configuration from environment variables."""
175
+ if api_key := os.getenv("ANTHROPIC_API_KEY"):
176
+ # API key is handled separately
177
+ pass
178
+
179
+ if ssh_user := os.getenv("PYNETWORKINTEL_SSH_USER"):
180
+ self.config.ssh.username = ssh_user
181
+
182
+ if ssh_key := os.getenv("PYNETWORKINTEL_SSH_KEY"):
183
+ self.config.ssh.key_path = ssh_key
184
+
185
+ if ssh_pass := os.getenv("PYNETWORKINTEL_SSH_PASSWORD"):
186
+ self.config.ssh.password = ssh_pass
187
+
188
+ @staticmethod
189
+ def get_default_config() -> AppConfig:
190
+ """Get default configuration."""
191
+ return AppConfig()