Add local Windows Server inventory collection and validation; include core package
This commit is contained in:
@@ -2,22 +2,9 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
from samsara.models.inventory import validate_inventory, restrict_inventory_fields
|
||||
|
||||
MAX_IMPORT_BYTES = 10 * 1024 * 1024
|
||||
SCHEMA = Path(__file__).resolve().parents[2] / 'schemas' / 'inventory-v1.schema.json'
|
||||
# Inventory fields allowed through the report boundary. Command arguments, environment
|
||||
# variables and private key material are deliberately excluded.
|
||||
ALLOWED = {
|
||||
'roles': {'name', 'state'},
|
||||
'services': {'name', 'display_name', 'state', 'startup', 'account'},
|
||||
'network': {'state', 'local_address', 'local_port', 'remote_address', 'remote_port', 'pid', 'process', 'direction'},
|
||||
'tasks': {'name', 'path', 'enabled', 'account', 'triggers', 'state', 'associated_unit'},
|
||||
'software': {'name', 'publisher', 'version', 'installation_date'},
|
||||
'storage': {'type', 'name', 'path', 'capacity_gib', 'free_gib'},
|
||||
'certificates': {'subject', 'issuer', 'expires', 'thumbprint'},
|
||||
}
|
||||
SERVER_FIELDS = {'hostname', 'platform', 'operating_system', 'os_version', 'os_build', 'last_boot', 'ip_addresses', 'network_adapters', 'domain'}
|
||||
|
||||
|
||||
def load_inventory(path):
|
||||
@@ -32,16 +19,35 @@ def load_inventory(path):
|
||||
# A report wrapper is accepted, but its precomputed assessment is never trusted.
|
||||
if isinstance(data, dict) and 'inventory_result' in data:
|
||||
data = data['inventory_result']
|
||||
schema = json.loads(SCHEMA.read_text(encoding='utf-8'))
|
||||
errors = list(Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors(data))
|
||||
if errors:
|
||||
raise ValueError('Inventory does not match the v1 schema; required fields, types or check statuses are invalid.')
|
||||
validate_inventory(data)
|
||||
# Keep diagnostic contents out of logs and exported reports. Diagnostics may
|
||||
# contain command lines or credentials from external collection tools.
|
||||
data['checks'] = {name: {'status': check['status'], 'message':
|
||||
'Imported check status. Consult collector diagnostics separately; details excluded to limit secret exposure.'}
|
||||
for name, check in data['checks'].items()}
|
||||
data['server'] = {key: value for key, value in data['server'].items() if key in SERVER_FIELDS}
|
||||
data['inventory'] = {section: [{key: value for key, value in row.items() if key in ALLOWED[section]}
|
||||
for row in rows] for section, rows in data['inventory'].items()}
|
||||
return data
|
||||
safe_errors = {
|
||||
'access_denied': 'Current account could not read this scope.',
|
||||
'command_unavailable': 'Required command was unavailable.',
|
||||
'timeout': 'Section time limit exceeded.',
|
||||
'overall_timeout': 'Overall collection time limit exceeded.',
|
||||
'output_limit': 'Collection output limit exceeded.',
|
||||
'powershell_failed': 'PowerShell could not execute the collector.',
|
||||
'invalid_output': 'Invalid section output.',
|
||||
'collection_failed': 'A local query failed.'
|
||||
}
|
||||
checks = {}
|
||||
for name, check in data['checks'].items():
|
||||
clean = {'status': check['status'], 'message':
|
||||
'Imported check status. Source assertions are unverified; raw diagnostics excluded.'}
|
||||
for field in ('started_at', 'completed_at'):
|
||||
if field in check:
|
||||
clean[field] = check[field]
|
||||
if 'errors' in check:
|
||||
clean['errors'] = []
|
||||
for error in check['errors']:
|
||||
code = error['code'] if error['code'] in safe_errors else 'collection_failed'
|
||||
clean['errors'].append({'code': code, 'scope': 'Imported check', 'message': safe_errors[code]})
|
||||
if 'permission_limitations' in check:
|
||||
clean['permission_limitations'] = ['Collector reported limited read access; details excluded.'] if check['permission_limitations'] else []
|
||||
if 'limitations' in check:
|
||||
clean['limitations'] = ['Collector reported scope limitations; consult the inventory owner.'] if check['limitations'] else []
|
||||
checks[name] = clean
|
||||
data['checks'] = checks
|
||||
return restrict_inventory_fields(data)
|
||||
|
||||
193
samsara/collectors/windows.py
Normal file
193
samsara/collectors/windows.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Local Windows Server collector. No remoting, credentials or shell interpolation."""
|
||||
import argparse
|
||||
import ctypes
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
from samsara.collectors.linux import CollectionCancelled
|
||||
from samsara.core.detection import check_profile
|
||||
from samsara.models.inventory import inventory_schema, validate_inventory, restrict_inventory_fields
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[2] / 'powershell' / 'Get-ServerInventory.ps1'
|
||||
SECTIONS = ('system', 'roles', 'services', 'network', 'tasks', 'software', 'storage', 'shares', 'certificates')
|
||||
SERVER_BUILDS = {'14393': '2016', '17763': '2019', '20348': '2022', '26100': '2025'}
|
||||
MAX_OUTPUT = 10 * 1024 * 1024
|
||||
|
||||
|
||||
class WindowsCollectionError(RuntimeError):
|
||||
def __init__(self, code, message):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
def timestamp():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def powershell_executable():
|
||||
if platform.system() != 'Windows':
|
||||
raise ValueError('Local Windows collection requires an authorized Windows Server execution host.')
|
||||
if struct.calcsize('P') != 8:
|
||||
raise ValueError('Use 64-bit Python for Windows Server collection.')
|
||||
# Query Windows itself; never look up powershell.exe in PATH or trust SystemRoot.
|
||||
buffer = ctypes.create_unicode_buffer(32768)
|
||||
size = ctypes.windll.kernel32.GetSystemDirectoryW(buffer, len(buffer))
|
||||
if size == 0 or size >= len(buffer):
|
||||
raise ValueError('Unable to locate the Windows system directory.')
|
||||
executable = Path(buffer.value) / 'WindowsPowerShell' / 'v1.0' / 'powershell.exe'
|
||||
if not executable.is_file() or not SCRIPT.is_file():
|
||||
raise ValueError('Windows PowerShell 5.1 or bundled collector scripts are unavailable.')
|
||||
return executable
|
||||
|
||||
|
||||
def run_section(section, executable, cancelled=lambda: False, timeout=45):
|
||||
if section not in SECTIONS:
|
||||
raise ValueError('Unknown Windows inventory section.')
|
||||
if cancelled():
|
||||
raise CollectionCancelled()
|
||||
args = [str(executable), '-NoLogo', '-NoProfile', '-NonInteractive', '-File', str(SCRIPT), '-Section', section]
|
||||
environment = os.environ.copy()
|
||||
# Only inbox modules are needed. Do not autoload user-supplied modules or profiles.
|
||||
environment['PSModulePath'] = str(Path(executable).parent / 'Modules')
|
||||
with tempfile.TemporaryFile() as output:
|
||||
try:
|
||||
process = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=output,
|
||||
stderr=subprocess.DEVNULL, shell=False, env=environment,
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
except OSError:
|
||||
raise WindowsCollectionError('powershell_failed', 'Windows PowerShell could not start. Check executable permissions and local policy.') from None
|
||||
deadline = time.monotonic() + timeout
|
||||
try:
|
||||
while process.poll() is None:
|
||||
if cancelled():
|
||||
raise CollectionCancelled()
|
||||
if time.monotonic() >= deadline:
|
||||
raise WindowsCollectionError('timeout', 'The local section exceeded its time limit.')
|
||||
if os.fstat(output.fileno()).st_size > MAX_OUTPUT:
|
||||
raise WindowsCollectionError('output_limit', 'The local section exceeded its output limit.')
|
||||
time.sleep(.05)
|
||||
if cancelled():
|
||||
raise CollectionCancelled()
|
||||
if process.returncode:
|
||||
raise WindowsCollectionError('powershell_failed', 'The trusted script could not run. Check script signing policy, module availability and read permissions; raw diagnostics are excluded.')
|
||||
output.seek(0)
|
||||
raw = output.read(MAX_OUTPUT + 1)
|
||||
if len(raw) > MAX_OUTPUT:
|
||||
raise WindowsCollectionError('output_limit', 'The local section exceeded its output limit.')
|
||||
try:
|
||||
return json.loads(raw.decode('utf-8-sig'))
|
||||
except (UnicodeError, ValueError, RecursionError):
|
||||
raise WindowsCollectionError('invalid_output', 'PowerShell did not return a valid UTF-8 JSON section.') from None
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
|
||||
def validate_section(record, section):
|
||||
schema = inventory_schema()
|
||||
properties = dict(schema['properties']['checks']['additionalProperties']['properties'])
|
||||
properties['section'] = {'const': section}
|
||||
if section == 'system':
|
||||
properties['data'] = {'type': 'object', 'required': ['hostname','platform','os_build','product_type','os_version'],
|
||||
'properties': schema['properties']['server']['properties']}
|
||||
else:
|
||||
properties['data'] = schema['properties']['inventory']['properties']['storage' if section == 'shares' else section]
|
||||
section_schema = {'type': 'object', 'required': ['section','status','message','started_at','completed_at','errors','permission_limitations','limitations','data'], 'properties': properties}
|
||||
if not Draft202012Validator(section_schema, format_checker=FormatChecker()).is_valid(record):
|
||||
raise WindowsCollectionError('invalid_output', 'PowerShell returned a section that violates the inventory contract.')
|
||||
if record['completed_at'] < record['started_at']:
|
||||
# Both native timestamps use the same UTC format. This is not a trust check.
|
||||
raise WindowsCollectionError('invalid_output', 'PowerShell returned inconsistent section timestamps.')
|
||||
return record
|
||||
|
||||
|
||||
def verify_server(server, profile='auto'):
|
||||
year = SERVER_BUILDS.get(server.get('os_build'))
|
||||
if server.get('platform') != 'windows' or server.get('product_type') not in (2, 3) or year is None:
|
||||
raise ValueError('Unable to verify Windows Server 2016, 2019, 2022 or 2025. Workstation editions and unknown builds are not supported.')
|
||||
if server.get('os_version') != year:
|
||||
raise ValueError('Windows Server identity fields are inconsistent; no assessment was accepted.')
|
||||
check_profile({'family': 'windows', 'version': year, 'name': server.get('operating_system', 'Windows Server ' + year)}, profile)
|
||||
|
||||
|
||||
def failure_check(code, message, section, start, status='failed'):
|
||||
return {'status': status, 'message': message, 'started_at': start, 'completed_at': timestamp(),
|
||||
'errors': [{'code': code, 'scope': section, 'message': message}],
|
||||
'permission_limitations': ['Permissions could not be verified for this check.'],
|
||||
'limitations': ['No successful evidence was substituted for this failure.']}
|
||||
|
||||
|
||||
def collect_local_windows(stage=lambda message: None, cancelled=lambda: False, profile='auto',
|
||||
section_timeout=45, total_timeout=300, runner=None):
|
||||
if platform.system() != 'Windows':
|
||||
raise ValueError('Local Windows collection requires an authorized Windows Server execution host.')
|
||||
if profile != 'auto' and not profile.startswith('windows:'):
|
||||
raise ValueError('Select Auto-detect or a Windows Server profile for this execution host.')
|
||||
executable = powershell_executable()
|
||||
runner = runner or run_section
|
||||
started = time.monotonic()
|
||||
result = {'schema_version': '1.0', 'synthetic': False, 'collected_at': timestamp(), 'server': {},
|
||||
'checks': {}, 'inventory': {key: [] for key in ('roles','services','network','tasks','software','storage','certificates')}}
|
||||
total_bytes = 0
|
||||
for section in SECTIONS:
|
||||
if cancelled():
|
||||
raise CollectionCancelled()
|
||||
stage('Collecting local Windows ' + section + '…')
|
||||
start = timestamp()
|
||||
remaining = total_timeout - (time.monotonic() - started)
|
||||
if remaining <= 0:
|
||||
result['checks'][section] = failure_check('overall_timeout', 'Overall collection time limit reached before this check.', section, start, 'not_collected')
|
||||
if section == 'system':
|
||||
raise ValueError('The Windows identity check could not run within the time limit.')
|
||||
continue
|
||||
try:
|
||||
record = validate_section(runner(section, executable, cancelled=cancelled, timeout=min(section_timeout, remaining)), section)
|
||||
total_bytes += len(json.dumps(record).encode('utf-8'))
|
||||
if total_bytes > MAX_OUTPUT:
|
||||
raise WindowsCollectionError('output_limit', 'Total inventory exceeded the 10 MiB limit; this section was omitted.')
|
||||
check = {key: record[key] for key in ('status','message','started_at','completed_at','errors','permission_limitations','limitations')}
|
||||
result['checks'][section] = check
|
||||
if section == 'system':
|
||||
if record['status'] == 'failed':
|
||||
raise ValueError('Windows identity collection failed. Verify local CIM read access and Windows PowerShell policy.')
|
||||
verify_server(record['data'], profile)
|
||||
result['server'] = record['data']
|
||||
result['server']['collection_source'] = 'Local Windows read-only collection'
|
||||
elif section == 'shares':
|
||||
result['inventory']['storage'].extend(record['data'])
|
||||
else:
|
||||
result['inventory'][section] = record['data']
|
||||
except WindowsCollectionError as exc:
|
||||
result['checks'][section] = failure_check(exc.code, str(exc), section, start)
|
||||
if section == 'system':
|
||||
raise ValueError('Windows identity collection failed: ' + str(exc)) from None
|
||||
if cancelled():
|
||||
raise CollectionCancelled()
|
||||
return validate_inventory(restrict_inventory_fields(result, local_source='Local Windows read-only collection'))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Read-only local Windows Server inventory. No remoting is used.')
|
||||
parser.add_argument('--output', required=True, help='New local JSON file to create')
|
||||
parser.add_argument('--profile', choices=['auto','windows:2016','windows:2019','windows:2022','windows:2025'], default='auto')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
data = collect_local_windows(profile=args.profile)
|
||||
with open(args.output, 'x', encoding='utf-8') as output:
|
||||
json.dump(data, output, indent=2)
|
||||
print('Windows inventory saved. Review all failed/partial checks and permission limitations before retirement decisions.')
|
||||
except (OSError, ValueError, WindowsCollectionError):
|
||||
parser.exit(1, 'Windows inventory was not saved. Check supported local Server OS, Python architecture, script policy, permissions and a new writable output filename.\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
samsara/core/__init__.py
Normal file
1
samsara/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
49
samsara/core/detection.py
Normal file
49
samsara/core/detection.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""OS identification and explicit profile checks; never probes a remote host."""
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shlex
|
||||
|
||||
PROFILES = [
|
||||
('Auto-detect', 'auto'),
|
||||
('Windows Server 2016', 'windows:2016'),
|
||||
('Windows Server 2019', 'windows:2019'),
|
||||
('Windows Server 2022', 'windows:2022'),
|
||||
('Windows Server 2025', 'windows:2025'),
|
||||
('Red Hat Enterprise Linux 7 (legacy)', 'rhel:7'),
|
||||
('Red Hat Enterprise Linux 8', 'rhel:8'),
|
||||
('Red Hat Enterprise Linux 9', 'rhel:9'),
|
||||
('Red Hat Enterprise Linux 10', 'rhel:10'),
|
||||
('RHEL-compatible Linux', 'rhel:any'),
|
||||
]
|
||||
|
||||
|
||||
def detect_local_os():
|
||||
system = platform.system()
|
||||
if system == 'Linux':
|
||||
release = {}
|
||||
for line in Path('/etc/os-release').read_text(encoding='utf-8').splitlines():
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
release[key] = ' '.join(shlex.split(value))
|
||||
distro = release.get('ID', '')
|
||||
related = set(release.get('ID_LIKE', '').split())
|
||||
rhel = distro in {'rhel', 'rocky', 'almalinux', 'centos', 'ol'} or 'rhel' in related
|
||||
return {'platform': 'linux', 'family': 'rhel' if rhel else 'linux',
|
||||
'version': release.get('VERSION_ID', '').split('.')[0],
|
||||
'name': release.get('PRETTY_NAME', 'Linux')}
|
||||
if system == 'Windows':
|
||||
# Do not infer a Server edition from a desktop kernel/build number.
|
||||
return {'platform': 'windows', 'family': 'windows', 'version': '',
|
||||
'name': 'Windows ' + platform.win32_ver()[0]}
|
||||
raise ValueError('Local collection is available on Linux. Import an inventory file for other platforms.')
|
||||
|
||||
|
||||
def check_profile(detected, profile):
|
||||
if profile == 'auto':
|
||||
return
|
||||
valid = {value for _, value in PROFILES}
|
||||
if profile not in valid:
|
||||
raise ValueError('Unknown operating system profile.')
|
||||
family, version = profile.split(':')
|
||||
if detected['family'] != family or (version != 'any' and detected['version'] != version):
|
||||
raise ValueError(f"Detected {detected['name']}, which does not match the selected profile. Select Auto-detect to assess this computer.")
|
||||
20
samsara/core/startup.py
Normal file
20
samsara/core/startup.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Check common desktop prerequisites before Qt can abort the process."""
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def desktop_error():
|
||||
if not sys.platform.startswith('linux'):
|
||||
return None
|
||||
backend = os.environ.get('QT_QPA_PLATFORM', '').split(':')[0]
|
||||
uses_xcb = backend == 'xcb' or (not backend and os.environ.get('DISPLAY') and not os.environ.get('WAYLAND_DISPLAY'))
|
||||
if uses_xcb:
|
||||
try:
|
||||
ctypes.CDLL('libxcb-cursor.so.0')
|
||||
except OSError:
|
||||
return ('Samsara needs the Qt X11 cursor library. On Debian-based desktops run:\n'
|
||||
' sudo apt install libxcb-cursor0\n'
|
||||
'Then retry: python -m samsara.main\n'
|
||||
'For other Linux distributions install the package providing libxcb-cursor.so.0.')
|
||||
return None
|
||||
10
samsara/core/validation.py
Normal file
10
samsara/core/validation.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import re
|
||||
|
||||
def validate_hostname(value):
|
||||
value = value.strip().rstrip(".")
|
||||
if len(value) > 253 or not value or any(
|
||||
not re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?", label)
|
||||
for label in value.split(".")
|
||||
):
|
||||
raise ValueError("Enter a hostname or FQDN using letters, digits, hyphens and dots (no URLs or commands).")
|
||||
return value.lower()
|
||||
41
samsara/models/inventory.py
Normal file
41
samsara/models/inventory.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""The shared v1 inventory contract, used at live collection and import boundaries."""
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
|
||||
SCHEMA_PATH = Path(__file__).resolve().parents[2] / 'schemas' / 'inventory-v1.schema.json'
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def inventory_schema():
|
||||
return json.loads(SCHEMA_PATH.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def validate_inventory(data):
|
||||
validator = Draft202012Validator(inventory_schema(), format_checker=FormatChecker())
|
||||
if not validator.is_valid(data):
|
||||
raise ValueError('Inventory does not match the v1 schema; required fields, types or check statuses are invalid.')
|
||||
return data
|
||||
|
||||
|
||||
ALLOWED = {
|
||||
'roles': {'name', 'state', 'display_name', 'feature_type'},
|
||||
'services': {'name', 'display_name', 'state', 'startup', 'account', 'pid'},
|
||||
'network': {'state', 'local_address', 'local_port', 'remote_address', 'remote_port', 'pid', 'process', 'direction'},
|
||||
'tasks': {'name', 'path', 'enabled', 'account', 'triggers', 'state', 'associated_unit', 'executables'},
|
||||
'software': {'name', 'publisher', 'version', 'installation_date', 'registry_view'},
|
||||
'storage': {'type', 'name', 'path', 'capacity_gib', 'free_gib', 'filesystem', 'special'},
|
||||
'certificates': {'subject', 'issuer', 'expires', 'thumbprint', 'store'},
|
||||
}
|
||||
SERVER_FIELDS = {'hostname', 'platform', 'operating_system', 'os_version', 'os_build', 'kernel_version', 'product_type', 'domain_joined', 'last_boot', 'ip_addresses', 'network_adapters', 'domain'}
|
||||
|
||||
|
||||
def restrict_inventory_fields(data, local_source=None):
|
||||
"""Keep only metadata fields; never serialize raw CIM/task/service objects."""
|
||||
data["server"] = {key: value for key, value in data["server"].items() if key in SERVER_FIELDS}
|
||||
if local_source:
|
||||
data["server"]["collection_source"] = local_source
|
||||
data["inventory"] = {section: [{key: value for key, value in row.items() if key in ALLOWED[section]}
|
||||
for row in rows] for section, rows in data["inventory"].items()}
|
||||
return data
|
||||
@@ -16,6 +16,8 @@ def export_html(path, data, assessment):
|
||||
content = "<h1>Samsara assessment</h1><p>SYNTHETIC DEMO — no real collection performed.</p>" if data["synthetic"] else "<h1>Samsara assessment</h1><p>IMPORTED INVENTORY — source assertions not independently verified.</p>"
|
||||
if not data['synthetic'] and data['server'].get('collection_source') == 'Local Linux read-only collection':
|
||||
content = '<h1>Samsara assessment</h1><p>LOCAL LINUX INVENTORY — read-only snapshot of the execution host.</p>'
|
||||
if not data['synthetic'] and data['server'].get('collection_source') == 'Local Windows read-only collection':
|
||||
content = '<h1>Samsara assessment</h1><p>LOCAL WINDOWS INVENTORY — read-only snapshot of the execution host.</p>'
|
||||
content += "<p>Heuristic requiring administrator review. This report does not authorize retirement.</p>"
|
||||
summary = summarize(data, assessment)
|
||||
content += f"<h2>Summary</h2><p>Retirement risk estimate: {escape(summary['risk_level'])} ({summary['risk_score']}/100). Higher scores mean more retirement concerns.</p>"
|
||||
|
||||
@@ -8,6 +8,7 @@ from samsara.ui.branding import application_icon
|
||||
from samsara.collectors.demo import collect_demo, DEMO_HOST, LINUX_DEMO_HOST
|
||||
from samsara.collectors.importer import load_inventory
|
||||
from samsara.collectors.linux import collect_local_linux, CollectionCancelled
|
||||
from samsara.collectors.windows import collect_local_windows
|
||||
from samsara.core.detection import detect_local_os, check_profile, PROFILES
|
||||
from samsara.analyzers.assessment import assess
|
||||
from samsara.analyzers.summary import summarize
|
||||
@@ -28,11 +29,12 @@ class CollectionJob(QThread):
|
||||
if self.source == 'local':
|
||||
self.stage.emit('Detecting operating system…')
|
||||
detected = detect_local_os()
|
||||
if detected['platform'] != 'linux':
|
||||
raise ValueError('Windows live collection is not available yet. Open a saved inventory or a sample assessment.')
|
||||
check_profile(detected, self.profile)
|
||||
self.stage.emit('Detected ' + detected['name'] + '. Collecting inventory…')
|
||||
data = collect_local_linux(stage=self.stage.emit, cancelled=self.isInterruptionRequested)
|
||||
if detected['platform'] == 'windows':
|
||||
data = collect_local_windows(profile=self.profile, stage=self.stage.emit, cancelled=self.isInterruptionRequested)
|
||||
else:
|
||||
check_profile(detected, self.profile)
|
||||
data = collect_local_linux(stage=self.stage.emit, cancelled=self.isInterruptionRequested)
|
||||
elif self.source == 'file':
|
||||
self.stage.emit('Reading saved inventory…')
|
||||
data = load_inventory(self.inventory_path)
|
||||
@@ -308,7 +310,7 @@ class MainWindow(QMainWindow):
|
||||
reports.addStretch()
|
||||
settings = self.page('Settings', 'Collection availability and assessment behavior.')
|
||||
settings.addWidget(self.label('Windows Server 2016 / 2019 / 2022 / 2025\nRed Hat Enterprise Linux 7 (legacy) / 8 / 9 / 10 and compatible distributions'))
|
||||
settings.addWidget(self.label('Local Linux collection is available. Live Windows and remote collection are not available yet. Saved inventories and samples work on either desktop.', muted=True))
|
||||
settings.addWidget(self.label('Local Windows Server and Linux collectors are available. Windows validation on a supported host is pending. Remote collection is a separate follow-up. Saved inventories and samples work on either desktop.', muted=True))
|
||||
settings.addWidget(self.label('Read-only collection • No credentials stored • Session history only', muted=True))
|
||||
settings.addStretch()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user