498 lines
19 KiB
Dart
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),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|