import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:diagd/src/rust/api/diagnostics.dart'; class SecurityTab extends StatelessWidget { final SecurityAuditReport security; const SecurityTab({ super.key, required this.security, }); @override Widget build(BuildContext context) { final score = security.securityScore; final scoreColor = score >= 80 ? const Color(0xFF3FB950) : (score >= 50 ? const Color(0xFFD29922) : const Color(0xFFF85149)); final failedChecks = security.hardeningChecks.where((c) => !c.isSecure).length; final passedChecks = security.hardeningChecks.where((c) => c.isSecure).length; return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Security Score Hero Card Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(12), border: Border.all(color: scoreColor.withAlpha(70)), ), child: Row( children: [ Stack( alignment: Alignment.center, children: [ SizedBox( width: 80, height: 80, child: CircularProgressIndicator( value: score / 100.0, strokeWidth: 8, backgroundColor: const Color(0xFF21262D), valueColor: AlwaysStoppedAnimation(scoreColor), ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$score', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: scoreColor, ), ), const Text( '/100', style: TextStyle(fontSize: 10, color: Color(0xFF8B949E)), ), ], ), ], ), const SizedBox(width: 24), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Индекс защищенности системы (Security Score)', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFFE6EDF3), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: scoreColor.withAlpha(30), borderRadius: BorderRadius.circular(6), border: Border.all(color: scoreColor.withAlpha(80)), ), child: Text( score >= 80 ? 'ОТЛИЧНО' : (score >= 50 ? 'ТРЕБУЕТ ВНИМАНИЯ' : 'УЯЗВИМО'), style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: scoreColor, ), ), ), ], ), const SizedBox(height: 6), Text( 'Аудит сетевого периметра, прав суперпользователя SUID/SGID, параметров защиты ядра (ASLR, ptrace, eBPF) и конфигурации SSH.', style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)), ), const SizedBox(height: 12), Row( children: [ _buildMiniBadge('Открытых портов (0.0.0.0): ${security.exposedPorts.length}', security.exposedPorts.isNotEmpty ? const Color(0xFFD29922) : const Color(0xFF3FB950)), const SizedBox(width: 10), _buildMiniBadge('SUID аномалий: ${security.suidAnomalies.length}', security.suidAnomalies.isNotEmpty ? const Color(0xFFF85149) : const Color(0xFF3FB950)), const SizedBox(width: 10), _buildMiniBadge('Пройдено тестов: $passedChecks / ${security.hardeningChecks.length}', failedChecks > 0 ? const Color(0xFFD29922) : const Color(0xFF3FB950)), ], ), ], ), ), ], ), ), const SizedBox(height: 24), // 1. Exposed Listening Ports Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'СЕТЕВОЙ ПЕРИМЕТР (ПОРТЫ, СЛУШАЮЩИЕ 0.0.0.0 И ::)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 1.2, color: Color(0xFF8B949E), ), ), Text( 'Всего: ${security.exposedPorts.length}', style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)), ), ], ), const SizedBox(height: 10), if (security.exposedPorts.isEmpty) _buildEmptyState('Открытых вовне портов не обнаружено. Локальный сетевой стек защищен.', Icons.verified_user_rounded) else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: security.exposedPorts.length, separatorBuilder: (context, index) => const SizedBox(height: 8), itemBuilder: (context, index) { final port = security.exposedPorts[index]; return _buildPortCard(context, port); }, ), const SizedBox(height: 28), // 2. SUID / SGID Anomalies Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'ПОДОЗРИТЕЛЬНЫЕ SUID / SGID БИНАРНЫЕ ФАЙЛЫ', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 1.2, color: Color(0xFF8B949E), ), ), Text( 'Аномалий: ${security.suidAnomalies.length}', style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)), ), ], ), const SizedBox(height: 10), if (security.suidAnomalies.isEmpty) _buildEmptyState('Аномальных или небезопасных SUID/SGID файлов в общедоступных каталогах (/tmp, /dev/shm) не обнаружено.', Icons.check_circle_outline_rounded) else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: security.suidAnomalies.length, separatorBuilder: (context, index) => const SizedBox(height: 8), itemBuilder: (context, index) { final suid = security.suidAnomalies[index]; return _buildSuidCard(suid); }, ), const SizedBox(height: 28), // 3. Kernel & OS Hardening Checklist const Text( 'ЧЕК-ЛИСТ ЗАЩИТЫ ЯДРА И ОПЕРАЦИОННОЙ СИСТЕМЫ (HARDENING)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 1.2, color: Color(0xFF8B949E), ), ), const SizedBox(height: 10), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: security.hardeningChecks.length, separatorBuilder: (context, index) => const SizedBox(height: 8), itemBuilder: (context, index) { final check = security.hardeningChecks[index]; return _buildHardeningCard(context, check); }, ), ], ), ); } Widget _buildMiniBadge(String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withAlpha(20), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withAlpha(60)), ), child: Text( text, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color), ), ); } Widget _buildEmptyState(String text, IconData icon) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF238636).withAlpha(50)), ), child: Row( children: [ Icon(icon, color: const Color(0xFF3FB950), size: 22), const SizedBox(width: 12), Expanded( child: Text( text, style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)), ), ), ], ), ); } Widget _buildPortCard(BuildContext context, ListeningPortInfo port) { final (Color badgeColor, String badgeText) = switch (port.riskLevel) { Severity.critical => (const Color(0xFFF85149), 'CRITICAL'), Severity.warning => (const Color(0xFFD29922), 'WARNING'), Severity.info => (const Color(0xFF3FB950), 'INFO'), }; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(8), border: Border.all(color: badgeColor.withAlpha(60)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: badgeColor.withAlpha(25), borderRadius: BorderRadius.circular(6), border: Border.all(color: badgeColor.withAlpha(80)), ), child: Text( badgeText, style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: badgeColor), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( '${port.protocol.toUpperCase()} ${port.port}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xFFE6EDF3), ), ), const SizedBox(width: 10), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: const Color(0xFF21262D), borderRadius: BorderRadius.circular(4), ), child: Text( port.processName.isNotEmpty ? port.processName : 'unknown process', style: const TextStyle(fontSize: 12, color: Color(0xFF58A6FF), fontFamily: 'monospace'), ), ), if (port.pid != null) ...[ const SizedBox(width: 6), Text('(PID: ${port.pid})', style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E))), ], ], ), const SizedBox(height: 4), Text( 'Адрес прослушивания: ${port.localAddress}', style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)), ), if (port.recommendation.isNotEmpty) ...[ const SizedBox(height: 6), Row( children: [ const Icon(Icons.arrow_right_rounded, size: 18, color: Color(0xFFD29922)), Expanded( child: Text( port.recommendation, style: TextStyle(fontSize: 12, color: badgeColor), ), ), ], ), ], ], ), ), ], ), ); } Widget _buildSuidCard(SuidAnomalyInfo suid) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFF85149).withAlpha(60)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.warning_amber_rounded, color: Color(0xFFF85149), size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( suid.path, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFFE6EDF3), fontFamily: 'monospace', ), ), const SizedBox(height: 4), Text( 'Владелец: ${suid.owner} | Права: ${suid.permissions}', style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)), ), const SizedBox(height: 4), Text( suid.riskReason, style: const TextStyle(fontSize: 12, color: Color(0xFFF85149)), ), ], ), ), ], ), ); } Widget _buildHardeningCard(BuildContext context, SecurityCheckItem check) { final isPass = check.isSecure; final color = isPass ? const Color(0xFF3FB950) : const Color(0xFFF85149); return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withAlpha(50)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( isPass ? Icons.check_circle_rounded : Icons.cancel_rounded, color: color, size: 20, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( check.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFFE6EDF3), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: color.withAlpha(20), borderRadius: BorderRadius.circular(6), ), child: Text( check.status, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: color, ), ), ), ], ), const SizedBox(height: 4), Text( check.description, style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)), ), if (!isPass && check.remediation != null && check.remediation!.isNotEmpty) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF0D1117), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF30363D)), ), child: Row( children: [ const Icon(Icons.build_rounded, size: 14, color: Color(0xFFD29922)), const SizedBox(width: 8), Expanded( child: Text( check.remediation!, style: const TextStyle(fontSize: 12, color: Color(0xFFE6EDF3), fontFamily: 'monospace'), ), ), IconButton( icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)), tooltip: 'Скопировать команду', onPressed: () { Clipboard.setData(ClipboardData(text: check.remediation!)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Команда устранения скопирована в буфер'), duration: Duration(seconds: 2), ), ); }, ), ], ), ), ], ], ), ), ], ), ); } }