Files
diagd/lib/widgets/security_tab.dart
T

498 lines
19 KiB
Dart

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<Color>(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),
),
);
},
),
],
),
),
],
],
),
),
],
),
);
}
}