""" TSCM Report Generation Module Generates: 1. Client-safe PDF reports with executive summary 2. Technical annex (JSON + CSV) with device timelines and indicators DISCLAIMER: All reports include mandatory disclaimers. No packet data. No claims of confirmed surveillance. """ from __future__ import annotations import csv import io import logging from dataclasses import dataclass, field from datetime import datetime from utils.tscm.signal_classification import ( SIGNAL_ANALYSIS_DISCLAIMER, assess_signal, generate_hedged_statement, ) logger = logging.getLogger('intercept.tscm.reports') # ============================================================================= # Report Data Structures # ============================================================================= @dataclass class ReportFinding: """A single finding for the report.""" identifier: str protocol: str name: str | None risk_level: str risk_score: int description: str indicators: list[dict] = field(default_factory=list) recommended_action: str = '' playbook_reference: str = '' # Signal classification data signal_strength: str | None = None # minimal, weak, moderate, strong, very_strong signal_confidence: str | None = None # low, medium, high signal_interpretation: str | None = None signal_caveats: list[str] = field(default_factory=list) @dataclass class ReportMeetingSummary: """Meeting window summary for report.""" name: str | None start_time: str end_time: str | None duration_minutes: float devices_first_seen: int behavior_changes: int high_interest_devices: int @dataclass class TSCMReport: """ Complete TSCM sweep report. Contains all data needed for both client-safe PDF and technical annex. """ # Report metadata report_id: str generated_at: datetime sweep_id: int sweep_type: str # Location and context location: str | None = None baseline_id: int | None = None baseline_name: str | None = None # Executive summary executive_summary: str = '' overall_risk_assessment: str = 'low' # low, moderate, elevated, high key_findings_count: int = 0 # Capabilities used capabilities: dict = field(default_factory=dict) limitations: list[str] = field(default_factory=list) # Findings by risk tier high_interest_findings: list[ReportFinding] = field(default_factory=list) needs_review_findings: list[ReportFinding] = field(default_factory=list) informational_findings: list[ReportFinding] = field(default_factory=list) # Meeting window summaries meeting_summaries: list[ReportMeetingSummary] = field(default_factory=list) # Statistics total_devices_scanned: int = 0 wifi_devices: int = 0 wifi_clients: int = 0 bluetooth_devices: int = 0 rf_signals: int = 0 new_devices: int = 0 missing_devices: int = 0 # Sweep duration sweep_start: datetime | None = None sweep_end: datetime | None = None duration_minutes: float = 0.0 # Technical data (for annex only) device_timelines: list[dict] = field(default_factory=list) all_indicators: list[dict] = field(default_factory=list) baseline_diff: dict | None = None correlation_data: list[dict] = field(default_factory=list) # ============================================================================= # Disclaimer Text # ============================================================================= REPORT_DISCLAIMER = """ IMPORTANT DISCLAIMER This report documents the findings of a Technical Surveillance Countermeasures (TSCM) sweep conducted using electronic detection equipment. The following limitations and considerations apply: 1. DETECTION LIMITATIONS: No TSCM sweep can guarantee detection of all surveillance devices. Sophisticated devices may evade detection. 2. FINDINGS ARE INDICATORS: All findings represent patterns and indicators, NOT confirmed surveillance devices. Each finding requires professional interpretation and may have legitimate explanations. 3. ENVIRONMENTAL FACTORS: Wireless signals are affected by building construction, interference, and other environmental factors that may impact detection accuracy. 4. POINT-IN-TIME ASSESSMENT: This report reflects conditions at the time of the sweep. Conditions may change after the assessment. 5. NOT LEGAL ADVICE: This report does not constitute legal advice. Consult qualified legal counsel for guidance on surveillance-related matters. 6. PRIVACY CONSIDERATIONS: Some detected devices may be legitimate personal devices of authorized individuals. This report should be treated as confidential and distributed only to authorized personnel on a need-to-know basis. """ ANNEX_DISCLAIMER = """ TECHNICAL ANNEX DISCLAIMER This annex contains detailed technical data from the TSCM sweep. This data is provided for documentation and audit purposes. - No raw packet captures or intercepted communications are included - Device identifiers (MAC addresses) are included for tracking purposes - Signal strength values are approximate and environment-dependent - Timeline data is time-bucketed to preserve privacy - All interpretations require professional TSCM expertise This data should be handled according to organizational data protection policies and applicable privacy regulations. """ # ============================================================================= # Report Generation Functions # ============================================================================= def generate_executive_summary(report: TSCMReport) -> str: """Generate executive summary text.""" lines = [] # Opening lines.append(f"TSCM Sweep Report - {report.location or 'Location Not Specified'}") lines.append(f"Conducted: {report.sweep_start.strftime('%Y-%m-%d %H:%M') if report.sweep_start else 'Unknown'}") lines.append(f"Duration: {report.duration_minutes:.0f} minutes") lines.append("") # Overall assessment assessment_text = { 'low': 'No significant indicators of surveillance activity were detected.', 'moderate': 'Some devices require review but no confirmed surveillance indicators.', 'elevated': 'Multiple indicators warrant further investigation.', 'high': 'Significant indicators detected requiring immediate attention.', } lines.append(f"OVERALL ASSESSMENT: {report.overall_risk_assessment.upper()}") lines.append(assessment_text.get(report.overall_risk_assessment, '')) lines.append("") # Key statistics lines.append("SCAN STATISTICS:") lines.append(f" - Total devices scanned: {report.total_devices_scanned}") lines.append(f" - WiFi access points: {report.wifi_devices}") lines.append(f" - WiFi clients: {report.wifi_clients}") lines.append(f" - Bluetooth devices: {report.bluetooth_devices}") lines.append(f" - RF signals: {report.rf_signals}") lines.append("") # Findings summary lines.append("FINDINGS SUMMARY:") lines.append(f" - High Interest (require investigation): {len(report.high_interest_findings)}") lines.append(f" - Needs Review: {len(report.needs_review_findings)}") lines.append(f" - Informational: {len(report.informational_findings)}") lines.append("") # Baseline comparison if available if report.baseline_name: lines.append(f"BASELINE COMPARISON (vs '{report.baseline_name}'):") lines.append(f" - New devices: {report.new_devices}") lines.append(f" - Missing devices: {report.missing_devices}") lines.append("") # Meeting window summary if available if report.meeting_summaries: lines.append("MEETING WINDOW ACTIVITY:") for meeting in report.meeting_summaries: lines.append(f" - {meeting.name or 'Unnamed meeting'}: " f"{meeting.devices_first_seen} new devices, " f"{meeting.high_interest_devices} high interest") lines.append("") # Limitations if report.limitations: lines.append("SWEEP LIMITATIONS:") for limit in report.limitations[:3]: # Top 3 limitations lines.append(f" - {limit}") lines.append("") return "\n".join(lines) def generate_findings_section(findings: list[ReportFinding], title: str) -> str: """Generate a findings section for the report with confidence-safe language.""" if not findings: return f"{title}\n\nNo findings in this category.\n" lines = [title, "=" * len(title), ""] for i, finding in enumerate(findings, 1): lines.append(f"{i}. {finding.name or finding.identifier}") lines.append(f" Protocol: {finding.protocol.upper()}") lines.append(f" Identifier: {finding.identifier}") lines.append(f" Risk Score: {finding.risk_score}") # Signal classification with confidence if finding.signal_strength: confidence_label = (finding.signal_confidence or 'low').capitalize() strength_label = finding.signal_strength.replace('_', ' ').title() lines.append(f" Signal: {strength_label} (Confidence: {confidence_label})") lines.append(f" Assessment: {finding.description}") # Interpretation with hedged language if finding.signal_interpretation: lines.append(f" Interpretation: {finding.signal_interpretation}") if finding.indicators: lines.append(" Indicators:") for ind in finding.indicators[:5]: # Limit to 5 indicators lines.append(f" - {ind.get('type', 'unknown')}: {ind.get('description', '')}") lines.append(f" Recommended Action: {finding.recommended_action}") if finding.playbook_reference: lines.append(f" Reference: {finding.playbook_reference}") # Include relevant caveats for high-interest findings if finding.signal_caveats and finding.risk_level == 'high_interest': lines.append(" Note: " + finding.signal_caveats[0]) lines.append("") return "\n".join(lines) def generate_meeting_section(summaries: list[ReportMeetingSummary]) -> str: """Generate meeting window summary section.""" if not summaries: return "MEETING WINDOW SUMMARY\n\nNo meeting windows were marked during this sweep.\n" lines = ["MEETING WINDOW SUMMARY", "=" * 22, ""] for meeting in summaries: lines.append(f"Meeting: {meeting.name or 'Unnamed'}") lines.append(f" Time: {meeting.start_time} - {meeting.end_time or 'ongoing'}") lines.append(f" Duration: {meeting.duration_minutes:.0f} minutes") lines.append(f" Devices first seen during meeting: {meeting.devices_first_seen}") lines.append(f" Behavior changes detected: {meeting.behavior_changes}") lines.append(f" High interest devices active: {meeting.high_interest_devices}") if meeting.devices_first_seen > 0 or meeting.high_interest_devices > 0: lines.append(" NOTE: Meeting-correlated activity detected - see findings for details") lines.append("") lines.append("Meeting-correlated activity indicates temporal correlation only.") lines.append("Devices appearing during meetings may have legitimate explanations.") lines.append("") return "\n".join(lines) def generate_pdf_content(report: TSCMReport) -> str: """ Generate complete PDF report content. Returns plain text that can be converted to PDF. For actual PDF generation, use a library like reportlab or weasyprint. """ sections = [] # Header sections.append("=" * 70) sections.append("TECHNICAL SURVEILLANCE COUNTERMEASURES (TSCM) SWEEP REPORT") sections.append("=" * 70) sections.append("") sections.append(f"Report ID: {report.report_id}") sections.append(f"Generated: {report.generated_at.strftime('%Y-%m-%d %H:%M:%S')}") sections.append(f"Sweep ID: {report.sweep_id}") sections.append("") # Executive Summary sections.append("-" * 70) sections.append("EXECUTIVE SUMMARY") sections.append("-" * 70) sections.append(report.executive_summary or generate_executive_summary(report)) sections.append("") # High Interest Findings if report.high_interest_findings: sections.append("-" * 70) sections.append(generate_findings_section( report.high_interest_findings, "HIGH INTEREST FINDINGS" )) # Needs Review Findings if report.needs_review_findings: sections.append("-" * 70) sections.append(generate_findings_section( report.needs_review_findings, "FINDINGS REQUIRING REVIEW" )) # Meeting Window Summary if report.meeting_summaries: sections.append("-" * 70) sections.append(generate_meeting_section(report.meeting_summaries)) # Capabilities & Limitations sections.append("-" * 70) sections.append("SWEEP CAPABILITIES & LIMITATIONS") sections.append("=" * 33) sections.append("") if report.capabilities: caps = report.capabilities sections.append("Equipment Used:") if caps.get('wifi', {}).get('mode') != 'unavailable': sections.append(f" - WiFi: {caps.get('wifi', {}).get('mode', 'unknown')} mode") if caps.get('bluetooth', {}).get('mode') != 'unavailable': sections.append(f" - Bluetooth: {caps.get('bluetooth', {}).get('mode', 'unknown')}") if caps.get('rf', {}).get('available'): sections.append(f" - RF/SDR: {caps.get('rf', {}).get('device_type', 'unknown')}") sections.append("") if report.limitations: sections.append("Limitations:") for limit in report.limitations: sections.append(f" - {limit}") sections.append("") # Signal Analysis Note sections.append("-" * 70) sections.append("SIGNAL ANALYSIS METHODOLOGY") sections.append("=" * 27) sections.append(SIGNAL_ANALYSIS_DISCLAIMER.strip()) sections.append("") # Disclaimer sections.append("-" * 70) sections.append(REPORT_DISCLAIMER) # Footer sections.append("") sections.append("=" * 70) sections.append("END OF REPORT") sections.append("=" * 70) return "\n".join(sections) def generate_technical_annex_json(report: TSCMReport) -> dict: """ Generate technical annex as JSON. Contains detailed device timelines, all indicators, and raw data for audit and further analysis. """ return { 'annex_type': 'tscm_technical_annex', 'report_id': report.report_id, 'generated_at': report.generated_at.isoformat(), 'sweep_id': report.sweep_id, 'disclaimer': ANNEX_DISCLAIMER.strip(), 'sweep_details': { 'type': report.sweep_type, 'location': report.location, 'start_time': report.sweep_start.isoformat() if report.sweep_start else None, 'end_time': report.sweep_end.isoformat() if report.sweep_end else None, 'duration_minutes': report.duration_minutes, 'baseline_id': report.baseline_id, 'baseline_name': report.baseline_name, }, 'capabilities': report.capabilities, 'limitations': report.limitations, 'statistics': { 'total_devices': report.total_devices_scanned, 'wifi_devices': report.wifi_devices, 'wifi_clients': report.wifi_clients, 'bluetooth_devices': report.bluetooth_devices, 'rf_signals': report.rf_signals, 'new_devices': report.new_devices, 'missing_devices': report.missing_devices, 'high_interest_count': len(report.high_interest_findings), 'needs_review_count': len(report.needs_review_findings), 'informational_count': len(report.informational_findings), }, 'findings': { 'high_interest': [ { 'identifier': f.identifier, 'protocol': f.protocol, 'name': f.name, 'risk_score': f.risk_score, 'description': f.description, 'indicators': f.indicators, 'recommended_action': f.recommended_action, 'signal_classification': { 'strength': f.signal_strength, 'confidence': f.signal_confidence, 'interpretation': f.signal_interpretation, 'caveats': f.signal_caveats, }, } for f in report.high_interest_findings ], 'needs_review': [ { 'identifier': f.identifier, 'protocol': f.protocol, 'name': f.name, 'risk_score': f.risk_score, 'description': f.description, 'indicators': f.indicators, 'signal_classification': { 'strength': f.signal_strength, 'confidence': f.signal_confidence, 'interpretation': f.signal_interpretation, 'caveats': f.signal_caveats, }, } for f in report.needs_review_findings ], }, 'meeting_windows': [ { 'name': m.name, 'start_time': m.start_time, 'end_time': m.end_time, 'duration_minutes': m.duration_minutes, 'devices_first_seen': m.devices_first_seen, 'behavior_changes': m.behavior_changes, 'high_interest_devices': m.high_interest_devices, } for m in report.meeting_summaries ], 'device_timelines': report.device_timelines, 'all_indicators': report.all_indicators, 'baseline_diff': report.baseline_diff, 'correlations': report.correlation_data, } def generate_technical_annex_csv(report: TSCMReport) -> str: """ Generate device timeline data as CSV. Provides spreadsheet-compatible format for further analysis. """ output = io.StringIO() writer = csv.writer(output) # Header writer.writerow([ 'identifier', 'protocol', 'name', 'risk_level', 'risk_score', 'first_seen', 'last_seen', 'observation_count', 'rssi_min', 'rssi_max', 'rssi_mean', 'rssi_stability', 'movement_pattern', 'meeting_correlated', 'indicators', ]) # Device data from timelines for timeline in report.device_timelines: indicators_str = '; '.join( f"{i.get('type', '')}({i.get('score', 0)})" for i in timeline.get('indicators', []) ) signal = timeline.get('signal', {}) metrics = timeline.get('metrics', {}) movement = timeline.get('movement', {}) meeting = timeline.get('meeting_correlation', {}) writer.writerow([ timeline.get('identifier', ''), timeline.get('protocol', ''), timeline.get('name', ''), timeline.get('risk_level', 'informational'), timeline.get('risk_score', 0), metrics.get('first_seen', ''), metrics.get('last_seen', ''), metrics.get('total_observations', 0), signal.get('rssi_min', ''), signal.get('rssi_max', ''), signal.get('rssi_mean', ''), signal.get('stability', ''), movement.get('pattern', ''), meeting.get('correlated', False), indicators_str, ]) # Also add findings summary writer.writerow([]) writer.writerow(['--- FINDINGS SUMMARY ---']) writer.writerow([ 'identifier', 'protocol', 'risk_level', 'risk_score', 'signal_strength', 'signal_confidence', 'description', 'interpretation', 'recommended_action' ]) all_findings = ( report.high_interest_findings + report.needs_review_findings ) for finding in all_findings: writer.writerow([ finding.identifier, finding.protocol, finding.risk_level, finding.risk_score, finding.signal_strength or '', finding.signal_confidence or '', finding.description, finding.signal_interpretation or '', finding.recommended_action, ]) return output.getvalue() # ============================================================================= # Report Builder # ============================================================================= class TSCMReportBuilder: """ Builder for constructing TSCM reports from sweep data. Usage: builder = TSCMReportBuilder(sweep_id=123) builder.set_location("Conference Room A") builder.add_capabilities(capabilities_dict) builder.add_finding(finding) report = builder.build() """ def __init__(self, sweep_id: int): self.sweep_id = sweep_id self.report = TSCMReport( report_id=f"TSCM-{sweep_id}-{datetime.now().strftime('%Y%m%d%H%M%S')}", generated_at=datetime.now(), sweep_id=sweep_id, sweep_type='standard', ) def set_sweep_type(self, sweep_type: str) -> TSCMReportBuilder: self.report.sweep_type = sweep_type return self def set_location(self, location: str) -> TSCMReportBuilder: self.report.location = location return self def set_baseline(self, baseline_id: int, baseline_name: str) -> TSCMReportBuilder: self.report.baseline_id = baseline_id self.report.baseline_name = baseline_name return self def set_sweep_times( self, start: datetime, end: datetime | None = None ) -> TSCMReportBuilder: self.report.sweep_start = start self.report.sweep_end = end or datetime.now() self.report.duration_minutes = ( (self.report.sweep_end - self.report.sweep_start).total_seconds() / 60 ) return self def add_capabilities(self, capabilities: dict) -> TSCMReportBuilder: self.report.capabilities = capabilities self.report.limitations = capabilities.get('all_limitations', []) return self def add_finding(self, finding: ReportFinding) -> TSCMReportBuilder: if finding.risk_level == 'high_interest': self.report.high_interest_findings.append(finding) elif finding.risk_level in ['review', 'needs_review']: self.report.needs_review_findings.append(finding) else: self.report.informational_findings.append(finding) return self def add_findings_from_profiles(self, profiles: list[dict]) -> TSCMReportBuilder: """Add findings from correlation engine device profiles.""" for profile in profiles: # Get signal classification data signal_data = self._classify_finding_signal(profile) finding = ReportFinding( identifier=profile.get('identifier', ''), protocol=profile.get('protocol', ''), name=profile.get('name'), risk_level=profile.get('risk_level', 'informational'), risk_score=profile.get('total_score', 0), description=self._generate_finding_description(profile), indicators=profile.get('indicators', []), recommended_action=profile.get('recommended_action', 'monitor'), playbook_reference=self._get_playbook_reference(profile), signal_strength=signal_data['signal_strength'], signal_confidence=signal_data['signal_confidence'], signal_interpretation=signal_data['signal_interpretation'], signal_caveats=signal_data['signal_caveats'], ) self.add_finding(finding) return self def _generate_finding_description(self, profile: dict) -> str: """Generate description from profile indicators using hedged language.""" indicators = profile.get('indicators', []) protocol = profile.get('protocol', 'Unknown').upper() # Get signal data for context rssi = profile.get('rssi_mean') or profile.get('rssi') duration = profile.get('observation_duration_seconds') observation_count = profile.get('observation_count', 1) # Assess signal to determine confidence assessment = assess_signal(rssi, duration, observation_count) confidence = assessment.confidence if not indicators: # Use hedged language based on confidence return generate_hedged_statement( f"Observed {protocol} signal", 'device_presence', confidence ) # Build description with hedged language primary = indicators[0] indicator_type = primary.get('type', 'pattern') # Map indicator types to hedged descriptions if indicator_type in ('airtag_detected', 'tile_detected', 'smarttag_detected', 'known_tracker'): desc = generate_hedged_statement( f"{protocol} signal characteristics", 'device_presence', confidence ) desc += f" - pattern consistent with {indicator_type.replace('_', ' ')}" elif indicator_type == 'audio_capable': desc = generate_hedged_statement( "Device characteristics", 'surveillance_indicator', confidence ) desc += " - audio-capable device type identified" elif indicator_type in ('hidden_identity', 'hidden_ssid'): desc = generate_hedged_statement( "Network configuration", 'surveillance_indicator', confidence ) desc += " - concealed identity pattern observed" else: desc = generate_hedged_statement( f"{protocol} signal pattern", 'device_presence', confidence ) if len(indicators) > 1: desc += f" (+{len(indicators) - 1} additional indicators)" return desc def _classify_finding_signal(self, profile: dict) -> dict: """Extract signal classification data for a finding.""" rssi = profile.get('rssi_mean') or profile.get('rssi') duration = profile.get('observation_duration_seconds') observation_count = profile.get('observation_count', 1) assessment = assess_signal(rssi, duration, observation_count) return { 'signal_strength': assessment.signal_strength.value, 'signal_confidence': assessment.confidence.value, 'signal_interpretation': assessment.interpretation, 'signal_caveats': assessment.caveats, } def _get_playbook_reference(self, profile: dict) -> str: """Get playbook reference based on profile.""" risk_level = profile.get('risk_level', 'informational') indicators = profile.get('indicators', []) # Check for tracker tracker_types = ['airtag_detected', 'tile_detected', 'smarttag_detected', 'known_tracker'] if any(i.get('type') in tracker_types for i in indicators) and risk_level == 'high_interest': return 'PB-001 (Tracker Detection)' if risk_level == 'high_interest': return 'PB-002 (Suspicious Device)' elif risk_level in ['review', 'needs_review']: return 'PB-003 (Unknown Device)' return '' def add_meeting_summary(self, summary: dict) -> TSCMReportBuilder: """Add meeting window summary.""" meeting = ReportMeetingSummary( name=summary.get('name'), start_time=summary.get('start_time', ''), end_time=summary.get('end_time'), duration_minutes=summary.get('duration_minutes', 0), devices_first_seen=summary.get('devices_first_seen', 0), behavior_changes=summary.get('behavior_changes', 0), high_interest_devices=summary.get('high_interest_devices', 0), ) self.report.meeting_summaries.append(meeting) return self def add_statistics( self, wifi: int = 0, wifi_clients: int = 0, bluetooth: int = 0, rf: int = 0, new: int = 0, missing: int = 0 ) -> TSCMReportBuilder: self.report.wifi_devices = wifi self.report.wifi_clients = wifi_clients self.report.bluetooth_devices = bluetooth self.report.rf_signals = rf self.report.total_devices_scanned = wifi + wifi_clients + bluetooth + rf self.report.new_devices = new self.report.missing_devices = missing return self def add_device_timelines(self, timelines: list[dict]) -> TSCMReportBuilder: self.report.device_timelines = timelines return self def add_all_indicators(self, indicators: list[dict]) -> TSCMReportBuilder: self.report.all_indicators = indicators return self def add_baseline_diff(self, diff: dict) -> TSCMReportBuilder: self.report.baseline_diff = diff return self def add_correlations(self, correlations: list[dict]) -> TSCMReportBuilder: self.report.correlation_data = correlations return self def build(self) -> TSCMReport: """Build and return the complete report.""" # Calculate overall risk assessment if self.report.high_interest_findings: if len(self.report.high_interest_findings) >= 3: self.report.overall_risk_assessment = 'high' else: self.report.overall_risk_assessment = 'elevated' elif self.report.needs_review_findings: self.report.overall_risk_assessment = 'moderate' else: self.report.overall_risk_assessment = 'low' self.report.key_findings_count = ( len(self.report.high_interest_findings) + len(self.report.needs_review_findings) ) # Generate executive summary self.report.executive_summary = generate_executive_summary(self.report) return self.report # ============================================================================= # Report Generation API Functions # ============================================================================= def generate_report( sweep_id: int, sweep_data: dict, device_profiles: list[dict], capabilities: dict, timelines: list[dict], baseline_diff: dict | None = None, meeting_summaries: list[dict] | None = None, correlations: list[dict] | None = None, ) -> TSCMReport: """ Generate a complete TSCM report from sweep data. Args: sweep_id: Sweep ID sweep_data: Sweep dict from database device_profiles: List of DeviceProfile dicts from correlation engine capabilities: Capabilities dict timelines: Device timeline dicts baseline_diff: Optional baseline diff dict meeting_summaries: Optional meeting summaries correlations: Optional correlation data Returns: Complete TSCMReport """ builder = TSCMReportBuilder(sweep_id) # Basic info builder.set_sweep_type(sweep_data.get('sweep_type', 'standard')) # Parse times started_at = sweep_data.get('started_at') completed_at = sweep_data.get('completed_at') if started_at: if isinstance(started_at, str): started_at = datetime.fromisoformat(started_at.replace('Z', '+00:00')).replace(tzinfo=None) if completed_at and isinstance(completed_at, str): completed_at = datetime.fromisoformat(completed_at.replace('Z', '+00:00')).replace(tzinfo=None) builder.set_sweep_times(started_at, completed_at) # Capabilities builder.add_capabilities(capabilities) # Add findings from profiles builder.add_findings_from_profiles(device_profiles) # Statistics results = sweep_data.get('results', {}) wifi_count = results.get('wifi_count') if wifi_count is None: wifi_count = len(results.get('wifi_devices', results.get('wifi', []))) wifi_client_count = results.get('wifi_client_count') if wifi_client_count is None: wifi_client_count = len(results.get('wifi_clients', [])) bt_count = results.get('bt_count') if bt_count is None: bt_count = len(results.get('bt_devices', results.get('bluetooth', []))) rf_count = results.get('rf_count') if rf_count is None: rf_count = len(results.get('rf_signals', results.get('rf', []))) builder.add_statistics( wifi=wifi_count, wifi_clients=wifi_client_count, bluetooth=bt_count, rf=rf_count, new=baseline_diff.get('summary', {}).get('new_devices', 0) if baseline_diff else 0, missing=baseline_diff.get('summary', {}).get('missing_devices', 0) if baseline_diff else 0, ) # Technical data builder.add_device_timelines(timelines) if baseline_diff: builder.add_baseline_diff(baseline_diff) if meeting_summaries: for summary in meeting_summaries: builder.add_meeting_summary(summary) if correlations: builder.add_correlations(correlations) # Extract all indicators all_indicators = [] for profile in device_profiles: for ind in profile.get('indicators', []): all_indicators.append({ 'device': profile.get('identifier'), 'protocol': profile.get('protocol'), **ind }) builder.add_all_indicators(all_indicators) return builder.build() def get_pdf_report(report: TSCMReport) -> str: """Get PDF-ready report content.""" return generate_pdf_content(report) def get_json_annex(report: TSCMReport) -> dict: """Get JSON technical annex.""" return generate_technical_annex_json(report) def get_csv_annex(report: TSCMReport) -> str: """Get CSV technical annex.""" return generate_technical_annex_csv(report)