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/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())
|
pynetworkintel/config.py
ADDED
|
@@ -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()
|