feat: initial commit of diagd - unified Linux/BSD/macOS/Windows diagnostic tool (TUI + GUI)
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:diagd/src/rust/api/diagnostics.dart';
|
||||
|
||||
class CrashesTab extends StatefulWidget {
|
||||
final List<CrashReportInfo> crashes;
|
||||
|
||||
const CrashesTab({
|
||||
super.key,
|
||||
required this.crashes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CrashesTab> createState() => _CrashesTabState();
|
||||
}
|
||||
|
||||
class _CrashesTabState extends State<CrashesTab> {
|
||||
int _selectedCrashIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.crashes.isEmpty) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 550),
|
||||
margin: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161B22),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF238636).withAlpha(80)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF238636).withAlpha(30),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.verified_rounded,
|
||||
color: Color(0xFF3FB950),
|
||||
size: 44,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
'Аварийных падений не зафиксировано',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFE6EDF3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'В системных журналах coredumpctl, /var/crash, dmesg и crash reporter отсутствуют записи о падениях процессов по сигналам SIGSEGV, SIGABRT или сбоям ядра.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFF8B949E), height: 1.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final selectedCrash = widget.crashes[_selectedCrashIndex.clamp(0, widget.crashes.length - 1)];
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 850;
|
||||
|
||||
if (isWide) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Left Column: Crash list
|
||||
SizedBox(
|
||||
width: 340,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildListHeader(),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: widget.crashes.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final crash = widget.crashes[index];
|
||||
final isSelected = index == _selectedCrashIndex;
|
||||
return _buildCrashListItem(crash, isSelected, () {
|
||||
setState(() {
|
||||
_selectedCrashIndex = index;
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
// Right Column: Crash details
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: _buildCrashDetails(context, selectedCrash),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildListHeader(),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.crashes.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final crash = widget.crashes[index];
|
||||
final isSelected = index == _selectedCrashIndex;
|
||||
return SizedBox(
|
||||
width: 260,
|
||||
child: _buildCrashListItem(crash, isSelected, () {
|
||||
setState(() {
|
||||
_selectedCrashIndex = index;
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildCrashDetails(context, selectedCrash),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'ЖУРНАЛ АВАРИЙНЫХ СБОЕВ',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: Color(0xFF8B949E),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF85149).withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
|
||||
),
|
||||
child: Text(
|
||||
'${widget.crashes.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFF85149),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCrashListItem(CrashReportInfo crash, bool isSelected, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF21262D) : const Color(0xFF161B22),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFFF85149) : const Color(0xFF30363D),
|
||||
width: isSelected ? 1.5 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF85149).withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
crash.signalOrCode,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFF85149),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
crash.processName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFE6EDF3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
crash.timestamp,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
|
||||
),
|
||||
if (crash.pid != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'PID: ${crash.pid}',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E), fontFamily: 'monospace'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCrashDetails(BuildContext context, CrashReportInfo crash) {
|
||||
final debugCmd = crash.dumpPath != null && crash.dumpPath!.isNotEmpty
|
||||
? 'gdb ${crash.processName} ${crash.dumpPath!}'
|
||||
: (crash.pid != null ? 'coredumpctl debug ${crash.pid}' : 'coredumpctl info ${crash.processName}');
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161B22),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFF30363D)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.bug_report_rounded, color: Color(0xFFF85149), size: 24),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
crash.processName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFE6EDF3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF85149).withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
|
||||
),
|
||||
child: Text(
|
||||
crash.signalOrCode,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFF85149),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (crash.pid != null)
|
||||
Text(
|
||||
'PID: ${crash.pid}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF8B949E),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Metadata Grid
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0D1117),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF21262D)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMetaRow('Время падения:', crash.timestamp),
|
||||
if (crash.faultModule != null && crash.faultModule!.isNotEmpty)
|
||||
_buildMetaRow('Сбойный модуль:', crash.faultModule!),
|
||||
if (crash.dumpPath != null && crash.dumpPath!.isNotEmpty) ...[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 130,
|
||||
child: Text(
|
||||
'Файл Core Dump:',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
crash.dumpPath!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF58A6FF),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_rounded, size: 14, color: Color(0xFF8B949E)),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
tooltip: 'Копировать путь к дампу',
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: crash.dumpPath!));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Путь к Core Dump скопирован'), duration: Duration(seconds: 2)),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Debug CLI Command
|
||||
const Text(
|
||||
'КОМАНДА ОТЛАДКИ (GDB / COREDUMPCTL)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.0,
|
||||
color: Color(0xFF8B949E),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0D1117),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF30363D)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'\$ ',
|
||||
style: TextStyle(color: Color(0xFF3FB950), fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
debugCmd,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFE6EDF3),
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)),
|
||||
tooltip: 'Копировать команду',
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: debugCmd));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Команда отладки скопирована'), duration: Duration(seconds: 2)),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Stack Trace / Backtrace
|
||||
const Text(
|
||||
'СТЕК ВЫЗОВОВ (BACKTRACE / CRASH TRACE)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.0,
|
||||
color: Color(0xFF8B949E),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0D1117),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF21262D)),
|
||||
),
|
||||
child: crash.stackTraceSnippet.isNotEmpty
|
||||
? SelectableText(
|
||||
crash.stackTraceSnippet,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFE6EDF3),
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
height: 1.4,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Трассировка стека недоступна в данном дампе памяти.',
|
||||
style: TextStyle(color: Color(0xFF8B949E), fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetaRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFE6EDF3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,545 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:diagd/src/rust/api/diagnostics.dart';
|
||||
|
||||
class LiveMonitorTab extends StatefulWidget {
|
||||
const LiveMonitorTab({super.key});
|
||||
|
||||
@override
|
||||
State<LiveMonitorTab> createState() => _LiveMonitorTabState();
|
||||
}
|
||||
|
||||
class _LiveMonitorTabState extends State<LiveMonitorTab> {
|
||||
Timer? _ticker;
|
||||
LiveMetricsSnapshot? _snapshot;
|
||||
final List<double> _cpuHistory = [];
|
||||
final List<double> _ramHistory = [];
|
||||
final List<double> _tempHistory = [];
|
||||
final List<double> _netHistory = [];
|
||||
final int _maxHistory = 60;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchMetrics();
|
||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) => _fetchMetrics());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchMetrics() async {
|
||||
try {
|
||||
final snap = await getLiveMetrics();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_snapshot = snap;
|
||||
_cpuHistory.add(snap.cpuUsagePercent.clamp(0.0, 100.0));
|
||||
if (_cpuHistory.length > _maxHistory) _cpuHistory.removeAt(0);
|
||||
|
||||
final ramPct = snap.ramTotalBytes > BigInt.zero
|
||||
? (snap.ramUsedBytes.toDouble() / snap.ramTotalBytes.toDouble()) * 100.0
|
||||
: 0.0;
|
||||
_ramHistory.add(ramPct.clamp(0.0, 100.0));
|
||||
if (_ramHistory.length > _maxHistory) _ramHistory.removeAt(0);
|
||||
|
||||
_tempHistory.add(snap.maxCpuTempC.clamp(0.0, 120.0));
|
||||
if (_tempHistory.length > _maxHistory) _tempHistory.removeAt(0);
|
||||
|
||||
final netKb = (snap.netRxKbps + snap.netTxKbps).toDouble();
|
||||
_netHistory.add(netKb);
|
||||
if (_netHistory.length > _maxHistory) _netHistory.removeAt(0);
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_snapshot == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF58A6FF)),
|
||||
);
|
||||
}
|
||||
|
||||
final snap = _snapshot!;
|
||||
final ramUsedGb = snap.ramUsedBytes.toDouble() / (1024 * 1024 * 1024);
|
||||
final ramTotalGb = snap.ramTotalBytes.toDouble() / (1024 * 1024 * 1024);
|
||||
final ramPct = ramTotalGb > 0 ? (ramUsedGb / ramTotalGb) * 100.0 : 0.0;
|
||||
|
||||
final swapUsedGb = snap.swapUsedBytes.toDouble() / (1024 * 1024 * 1024);
|
||||
final swapTotalGb = snap.swapTotalBytes.toDouble() / (1024 * 1024 * 1024);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Throttling Alert Banner
|
||||
if (snap.isThrottling) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF85149).withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFF85149)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning_rounded, color: Color(0xFFF85149), size: 28),
|
||||
SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'КРИТИЧЕСКИЙ ПЕРЕГРЕВ: АКТИВЕН THERMAL THROTTLING!',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF85149),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Процессор сбросил тактовые частоты из-за превышения температурного порога. Проверьте систему охлаждения и термопасту.',
|
||||
style: TextStyle(color: Color(0xFFE6EDF3), fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Top Metric Cards Grid
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final cols = constraints.maxWidth > 900 ? 4 : (constraints.maxWidth > 550 ? 2 : 1);
|
||||
return GridView.count(
|
||||
crossAxisCount: cols,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 14,
|
||||
mainAxisSpacing: 14,
|
||||
childAspectRatio: constraints.maxWidth > 900 ? 1.6 : 2.0,
|
||||
children: [
|
||||
_buildMetricCard(
|
||||
title: 'Загрузка CPU',
|
||||
value: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
|
||||
subvalue: 'Общая нагрузка ядер',
|
||||
icon: Icons.memory_rounded,
|
||||
color: snap.cpuUsagePercent > 85
|
||||
? const Color(0xFFF85149)
|
||||
: (snap.cpuUsagePercent > 60 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
|
||||
progress: snap.cpuUsagePercent / 100.0,
|
||||
),
|
||||
_buildMetricCard(
|
||||
title: 'Оперативная память (RAM)',
|
||||
value: '${ramUsedGb.toStringAsFixed(1)} / ${ramTotalGb.toStringAsFixed(1)} GB',
|
||||
subvalue: '${ramPct.toStringAsFixed(0)}% использовано',
|
||||
icon: Icons.storage_rounded,
|
||||
color: ramPct > 90
|
||||
? const Color(0xFFF85149)
|
||||
: (ramPct > 75 ? const Color(0xFFD29922) : const Color(0xFF58A6FF)),
|
||||
progress: ramPct / 100.0,
|
||||
),
|
||||
_buildMetricCard(
|
||||
title: 'Температура CPU',
|
||||
value: '${snap.maxCpuTempC.toStringAsFixed(1)} °C',
|
||||
subvalue: snap.isThrottling ? 'Троттлинг АКТИВЕН' : 'Охлаждение в норме',
|
||||
icon: Icons.thermostat_rounded,
|
||||
color: snap.maxCpuTempC > 85
|
||||
? const Color(0xFFF85149)
|
||||
: (snap.maxCpuTempC > 70 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
|
||||
progress: (snap.maxCpuTempC.clamp(20.0, 105.0) - 20.0) / 85.0,
|
||||
),
|
||||
_buildMetricCard(
|
||||
title: 'Дисковый и Сетевой I/O',
|
||||
value: '${((snap.diskReadKbps + snap.diskWriteKbps).toDouble() / 1024.0).toStringAsFixed(1)} MB/s',
|
||||
subvalue: 'Сеть: ${snap.netRxKbps + snap.netTxKbps} KB/s (RX+TX)',
|
||||
icon: Icons.swap_vert_rounded,
|
||||
color: const Color(0xFFA371F7),
|
||||
progress: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Realtime Sparklines / Charts
|
||||
const Text(
|
||||
'ДИНАМИКА НАГРУЗКИ В РЕАЛЬНОМ ВРЕМЕНИ (ПОСЛЕДНИЕ 60 СЕК)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: Color(0xFF8B949E),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 800;
|
||||
if (isWide) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildChartContainer(
|
||||
title: 'Нагрузка CPU (%)',
|
||||
color: const Color(0xFF58A6FF),
|
||||
history: _cpuHistory,
|
||||
maxVal: 100.0,
|
||||
currentVal: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: _buildChartContainer(
|
||||
title: 'Использование RAM (%)',
|
||||
color: const Color(0xFF3FB950),
|
||||
history: _ramHistory,
|
||||
maxVal: 100.0,
|
||||
currentVal: '${ramPct.toStringAsFixed(1)}%',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
_buildChartContainer(
|
||||
title: 'Нагрузка CPU (%)',
|
||||
color: const Color(0xFF58A6FF),
|
||||
history: _cpuHistory,
|
||||
maxVal: 100.0,
|
||||
currentVal: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildChartContainer(
|
||||
title: 'Использование RAM (%)',
|
||||
color: const Color(0xFF3FB950),
|
||||
history: _ramHistory,
|
||||
maxVal: 100.0,
|
||||
currentVal: '${ramPct.toStringAsFixed(1)}%',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 800;
|
||||
if (isWide) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildChartContainer(
|
||||
title: 'Температура CPU (°C)',
|
||||
color: const Color(0xFFD29922),
|
||||
history: _tempHistory,
|
||||
maxVal: 105.0,
|
||||
currentVal: '${snap.maxCpuTempC.toStringAsFixed(1)}°C',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: _buildChartContainer(
|
||||
title: 'Сетевой трафик (KB/s)',
|
||||
color: const Color(0xFFA371F7),
|
||||
history: _netHistory,
|
||||
maxVal: _netHistory.isEmpty ? 100.0 : max(100.0, _netHistory.reduce(max)),
|
||||
currentVal: '${snap.netRxKbps + snap.netTxKbps} KB/s',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
_buildChartContainer(
|
||||
title: 'Температура CPU (°C)',
|
||||
color: const Color(0xFFD29922),
|
||||
history: _tempHistory,
|
||||
maxVal: 105.0,
|
||||
currentVal: '${snap.maxCpuTempC.toStringAsFixed(1)}°C',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildChartContainer(
|
||||
title: 'Сетевой трафик (KB/s)',
|
||||
color: const Color(0xFFA371F7),
|
||||
history: _netHistory,
|
||||
maxVal: _netHistory.isEmpty ? 100.0 : max(100.0, _netHistory.reduce(max)),
|
||||
currentVal: '${snap.netRxKbps + snap.netTxKbps} KB/s',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Detailed Memory & Storage Row
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161B22),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFF30363D)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline_rounded, color: Color(0xFF58A6FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Детализация виртуальной памяти и I/O накопителей',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFE6EDF3),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildDetailRow(
|
||||
'Swap память:',
|
||||
'${swapUsedGb.toStringAsFixed(2)} GB из ${swapTotalGb.toStringAsFixed(2)} GB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildDetailRow(
|
||||
'Диск (Read / Write):',
|
||||
'${(snap.diskReadKbps.toDouble() / 1024.0).toStringAsFixed(1)} MB/s / ${(snap.diskWriteKbps.toDouble() / 1024.0).toStringAsFixed(1)} MB/s',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildDetailRow(
|
||||
'Сеть (RX / TX):',
|
||||
'${snap.netRxKbps} KB/s / ${snap.netTxKbps} KB/s',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required String subvalue,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
double? progress,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161B22),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: color.withAlpha(50)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF8B949E),
|
||||
),
|
||||
),
|
||||
Icon(icon, color: color, size: 20),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subvalue,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (progress != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
backgroundColor: const Color(0xFF21262D),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
minHeight: 4,
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartContainer({
|
||||
required String title,
|
||||
required Color color,
|
||||
required List<double> history,
|
||||
required double maxVal,
|
||||
required String currentVal,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161B22),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFF30363D)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFE6EDF3),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
currentVal,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 90,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _SparklinePainter(
|
||||
data: history,
|
||||
color: color,
|
||||
maxVal: maxVal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String val) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E))),
|
||||
const SizedBox(height: 3),
|
||||
Text(val, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFE6EDF3))),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SparklinePainter extends CustomPainter {
|
||||
final List<double> data;
|
||||
final Color color;
|
||||
final double maxVal;
|
||||
|
||||
_SparklinePainter({
|
||||
required this.data,
|
||||
required this.color,
|
||||
required this.maxVal,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (data.isEmpty) return;
|
||||
|
||||
final paintLine = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 2.0
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final paintFill = Paint()
|
||||
..shader = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
color.withAlpha(80),
|
||||
color.withAlpha(0),
|
||||
],
|
||||
).createShader(Rect.fromLTWH(0, 0, size.width, size.height))
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path = Path();
|
||||
final fillPath = Path();
|
||||
|
||||
final stepX = size.width / max(1, (60 - 1));
|
||||
final startIdx = 60 - data.length;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
final x = (startIdx + i) * stepX;
|
||||
final valNorm = (data[i] / (maxVal > 0 ? maxVal : 1.0)).clamp(0.0, 1.0);
|
||||
final y = size.height - (valNorm * size.height);
|
||||
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
fillPath.moveTo(x, size.height);
|
||||
fillPath.lineTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
fillPath.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
fillPath.lineTo(size.width, size.height);
|
||||
fillPath.close();
|
||||
|
||||
canvas.drawPath(fillPath, paintFill);
|
||||
canvas.drawPath(path, paintLine);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _SparklinePainter oldDelegate) => true;
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user