#Requires -Version 5.1 <# Всёреш — проверка компьютера. Собирает сведения о Windows, железе, драйверах, обновлениях, ошибках, программах, автозагрузке и защите. Ничего не меняет в системе. Отчёт сохраняется ZIP-архивом в %LOCALAPPDATA%\Vseresh\reports; на сервер Всёреша (vsrsh.ru) он уходит, только если нажать «Отправить Всёрешу». Запуск: irm https://vsrsh.ru/run | iex Или из файла (Windows PowerShell 5.1): powershell -NoProfile -STA -ExecutionPolicy Bypass -File .\vseresh.ps1 -AutoStart — начать проверку сразу (так Всёреш перезапускает себя с правами администратора). -Server — другой адрес сервера, например http://192.168.1.10:8100 для проверки. Можно задать и переменной окружения VSRSH_SERVER. #> param([switch]$AutoStart, [string]$Server) $VseReshVersion = '0.3.4' $ReportSchema = 'vseresh.report/0.2' $VsScriptPath = $PSCommandPath $VsServer = if ($Server) { $Server } elseif ($env:VSRSH_SERVER) { $env:VSRSH_SERVER } else { 'https://vsrsh.ru' } $VsServer = $VsServer.TrimEnd('/') # Адрес попадает в URL запросов и в командную строку перезапуска с правами — только схема, имя и порт. if ($VsServer -notmatch '^https?://[A-Za-z0-9.-]+(:\d{1,5})?\z') { Write-Host ('Всёреш: непонятный адрес сервера «{0}» — использую https://vsrsh.ru' -f $VsServer) -ForegroundColor Yellow $VsServer = 'https://vsrsh.ru' } $VsServerHost = $VsServer -replace '^https?://', '' $ErrorActionPreference = 'Stop' # 32-битный PowerShell на 64-битной Windows видит другой реестр — перезапускаемся в 64-битном. if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess -and $PSCommandPath) { $native = Join-Path $env:WINDIR 'sysnative\WindowsPowerShell\v1.0\powershell.exe' if (Test-Path -LiteralPath $native) { $relaunch = @('-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath) if ($AutoStart) { $relaunch += '-AutoStart' } if ($Server) { $relaunch += @('-Server', $VsServer) } & $native @relaunch return } } if ([Threading.Thread]::CurrentThread.ApartmentState -ne 'STA') { Write-Host 'Окну Всёреша нужен режим STA. Запустите так:' -ForegroundColor Yellow Write-Host ' powershell -NoProfile -STA -ExecutionPolicy Bypass -File .\vseresh.ps1' return } # Старые сборки Windows 10 по умолчанию предлагают серверу только TLS 1.0 — добавляем 1.2. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } [Net.ServicePointManager]::Expect100Continue = $false Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Xaml # --------------------------------------------------------------------------- # Персонаж: отрисовка ASCII-Всёреша и его поведение. Считается в C#: # из чистого PowerShell 60 кадров в секунду не получить. $ghostSource = @' using System; using System.Collections.Generic; using System.Globalization; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; public class VseReshGhost { public const int W = 480, H = 352, CW = 7, CH = 10, COLS = 68, ROWS = 35, EscapeClicks = 7; public const double DW = 240, DH = 176; const double S = 150, CY = 176; const int MW = 24, MH = 30; public WriteableBitmap Bitmap; public string Mood = "idle", State = "idle"; public bool Paused, Reduced, Reading, PoseHidden; public double ReadYaw, ReadGaze, HomeLeft, PoseX, PoseY, PoseRoll; readonly int[] buf = new int[W * H]; readonly Dictionary masks = new Dictionary(); readonly Dictionary bounds = new Dictionary(); readonly System.Diagnostics.Stopwatch clock = System.Diagnostics.Stopwatch.StartNew(); double time, lastMs, since, speed = 1; int clicks, reaction; readonly Queue queue = new Queue(); const int X = 0, Y = 1, ROLL = 2, YAW = 3, HAND = 4, LAUGH = 5, COVER = 6, BEND = 7, GAZE = 8, N = 9; readonly double[] motion = new double[N], vel = new double[N]; bool seeded; double faceX = .16, faceY; readonly double[] hx = new double[2], hy = new double[2], hsh = new double[2]; // Палитра снята с рендера 3D-2K.jpg. static readonly int[][] RAMP = { new[] { 204, 72, 24 }, new[] { 242, 110, 40 }, new[] { 255, 140, 56 }, new[] { 255, 162, 78 }, new[] { 255, 182, 108 }, new[] { 255, 205, 152 } }; static readonly int[] EYE = { 34, 12, 9 }, EYE_HL = { 246, 242, 240 }, BLUSH = { 242, 128, 136 }, MOUTH = { 118, 26, 20 }, TONGUE = { 228, 96, 96 }; static readonly int[] WARM = { 226, 84, 40 }, SCAN = { 255, 236, 214 }, SPARK_OK = { 255, 210, 154 }, SPARK_RUN = { 239, 131, 96 }; const string GLYPHS = "=+*x%#@"; static readonly double LX, LY, LZ; static readonly string[] MODES = { "same", "alt", "up", "same", "alt", "shake" }; static VseReshGhost() { double n = Math.Sqrt(.45 * .45 + .6 * .6 + .66 * .66); LX = -.45 / n; LY = -.6 / n; LZ = .66 / n; } public VseReshGhost() { Bitmap = new WriteableBitmap(W, H, 96, 96, PixelFormats.Pbgra32, null); var tf = new Typeface(new FontFamily("Consolas"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal); foreach (char ch in "=+*x%#@o-·") { var dv = new DrawingVisual(); using (var dc = dv.RenderOpen()) { var ft = new FormattedText(ch.ToString(), CultureInfo.InvariantCulture, FlowDirection.LeftToRight, tf, 15, Brushes.White); dc.DrawText(ft, new Point((MW - ft.Width) / 2, (MH - ft.Height) / 2)); } var rtb = new RenderTargetBitmap(MW, MH, 96, 96, PixelFormats.Pbgra32); rtb.Render(dv); var px = new int[MW * MH]; rtb.CopyPixels(px, MW * 4, 0); var m = new byte[MW * MH]; int x0 = MW, x1 = -1, y0 = MH, y1 = -1; for (int i = 0; i < px.Length; i++) { int a = Math.Min(255, (int)(((px[i] >> 24) & 255) * 1.35)); // чуть жирнее, как обводка в превью m[i] = (byte)a; if (a > 0) { int xx = i % MW, yy = i / MW; x0 = Math.Min(x0, xx); x1 = Math.Max(x1, xx); y0 = Math.Min(y0, yy); y1 = Math.Max(y1, yy); } } masks[ch] = m; bounds[ch] = new[] { x0, x1, y0, y1 }; } lastMs = Now(); since = lastMs; } double Now() { return clock.Elapsed.TotalMilliseconds; } static double Clamp(double v, double a = 0, double b = 1) { return Math.Max(a, Math.Min(b, v)); } static double Smooth(double v) { v = Clamp(v); return v * v * (3 - 2 * v); } static double Sq(double v) { return v * v; } static void Set(int[] c, int[] t) { c[0] = t[0]; c[1] = t[1]; c[2] = t[2]; } static void Mix(int[] c, int[] t, double f) { for (int i = 0; i < 3; i++) c[i] = (int)Math.Round(c[i] + (t[i] - c[i]) * f); } // --- Поведение: щекотка → после 7-го клика убегает, прячется, выглядывает, возвращается. public bool Busy { get { return State == "escape" || State == "away" || State == "peek" || State == "return"; } } double Duration(string s) { switch (s) { case "tickle": return 2200 / speed; case "escape": return Reduced ? 120 : 1500; case "away": return 10000; case "peek": return 1800; case "return": return Reduced ? 120 : 3400; } return 0; } static string Next(string s) { switch (s) { case "tickle": return "idle"; case "escape": return "away"; case "away": return "peek"; case "peek": return "return"; } return "idle"; } void Enter(string s, double at) { State = s; since = at; } void UpdateActor(double now) { for (int i = 0; i < 20 && State != "idle"; i++) { double end = since + Duration(State); if (now < end) break; if (State == "tickle" && queue.Count > 0) { reaction = queue.Dequeue(); Enter("tickle", end); } else { if (State == "return") { speed = 1; queue.Clear(); clicks = 0; } Enter(Next(State), end); } } } public void Tickle() { double now = Now(); UpdateActor(now); if (State != "idle" && State != "tickle") return; clicks++; if (clicks >= EscapeClicks) { clicks = 0; queue.Clear(); speed = 1; Enter("escape", now); return; } int r = (clicks - 1) % 6; double elapsed = State == "tickle" ? (now - since) * speed : 0; speed = 1 + (clicks - 1) * .14; if (State == "tickle") { queue.Enqueue(r); Enter("tickle", now - elapsed / speed); } else { reaction = r; Enter("tickle", now); } } bool TargetPose(double now, double[] p) { UpdateActor(now); Array.Clear(p, 0, N); bool hidden = false; double t = Math.Max(0, (now - since) / 1000) * (State == "tickle" ? speed : 1); double q = State == "idle" ? 0 : Clamp((now - since) / Duration(State)); double exit = -HomeLeft - DW - 30, peek = -HomeLeft - DW * .5 + 8; if (State == "idle") { double ph = time % 16; p[YAW] = ph < 3 ? 0 : ph < 5 ? -.85 * Smooth((ph - 3) / .8) : ph < 7 ? -.85 * (1 - Smooth((ph - 6) / .8)) : ph < 10 ? .85 * Smooth((ph - 8) / .8) : ph < 12 ? .85 * (1 - Smooth((ph - 11) / .8)) : 0; if (Mood == "thinking") { p[YAW] = Math.Sin(time * .9) * .35; p[ROLL] = Math.Sin(time * 1.3) * 3; p[BEND] = Math.Sin(time * 1.3) * .05; p[HAND] = .08; } else if (Mood == "success") { double hop = Math.Abs(Math.Sin(time * 3.2)); p[Y] = -9 * hop; p[HAND] = .34 + Math.Sin(time * 6.4) * .05; p[YAW] = Math.Sin(time * 1.6) * .3; p[ROLL] = Math.Sin(time * 3.2) * 3; } else if (Mood == "verdict") { p[Y] = 6; p[BEND] = .07; p[ROLL] = -3; p[YAW] = -.2; p[GAZE] = .35; } if (Reading) { p[YAW] = ReadYaw; p[GAZE] = ReadGaze; p[Y] = 0; p[ROLL] = 0; } } else if (State == "tickle") { // Мягкая атака и долгое затухание: без резкой остановки. double env = Smooth(t / .32) * (1 - Smooth((t - 1.15) / 1.05)); p[LAUGH] = env; switch (reaction) { case 0: p[HAND] = .32 * env; p[X] = Math.Sin(t * 5) * 5 * env; p[BEND] = Math.Sin(t * 5) * .13 * env; p[ROLL] = Math.Sin(t * 5) * 3 * env; break; case 1: p[HAND] = .26 * env; p[X] = Math.Sin(t * 3.8) * 12 * env; p[ROLL] = Math.Sin(t * 3.8) * 8 * env; p[BEND] = -Math.Sin(t * 3.8) * .12 * env; p[YAW] = Math.Sin(t * 3) * .4 * env; break; case 2: p[HAND] = .3 * env; p[Y] = -8 * (1 - Math.Cos(t * 5)) * .5 * env; p[BEND] = Math.Sin(t * 3.5) * .09 * env; p[ROLL] = Math.Sin(t * 3.5) * 3 * env; break; case 3: p[COVER] = env; p[HAND] = .2 * env; p[YAW] = -.3 * env; p[ROLL] = -5 * env; p[BEND] = -.13 * env; break; case 4: p[YAW] = Math.Sin(t * 3.7) * .85 * env; p[HAND] = .25 * env; p[ROLL] = Math.Sin(t * 3.7) * 5 * env; p[BEND] = Math.Sin(t * 3.7) * .11 * env; p[X] = Math.Sin(t * 3.7) * 6 * env; break; case 5: p[HAND] = .15 * env; p[Y] = 3 * env; p[BEND] = .14 * env; p[ROLL] = (3 + Math.Sin(t * 4) * 2) * env; p[YAW] = .25 * env; break; } } else if (State == "escape") { double run = Smooth((q - .12) / .68), settle = 1 - Smooth((q - .78) / .22); p[YAW] = -1; p[HAND] = .3 * settle; p[LAUGH] = .6 * settle; p[X] = exit * run; p[Y] = -3 * (1 - Math.Cos(t * 12)) * settle; p[ROLL] = (-4 + Math.Sin(t * 10) * 3) * settle; p[BEND] = (-.08 + Math.Sin(t * 10) * .04) * settle; } else if (State == "away") { hidden = true; p[X] = exit; } else if (State == "peek") { p[X] = exit + (peek - exit) * Smooth(q / .4); p[YAW] = .9; p[ROLL] = 5; p[HAND] = .12; p[BEND] = .09; } else if (State == "return") { double walk = Smooth(q / .82), settle = 1 - Smooth((q - .73) / .27); p[X] = peek * (1 - walk); p[YAW] = .65 * settle; p[HAND] = .22 * settle; p[Y] = -2.5 * (1 - Math.Cos(t * 7)) * settle; p[ROLL] = Math.Sin(t * 5.5) * 6 * settle; p[BEND] = -Math.Sin(t * 5.5) * .09 * settle; } if (Reduced) { p[Y] = 0; p[ROLL] = 0; p[BEND] = 0; if (State == "tickle") { p[X] = 0; p[LAUGH] = 1; p[HAND] = .2; p[COVER] = reaction == 3 ? 1 : 0; } if (State == "escape") hidden = true; if (State == "peek") p[X] = peek; if (State == "return") p[X] = 0; } return hidden; } // Аналитическая критически демпфированная пружина: сохраняет скорость между кликами. static void Spring(ref double x, ref double v, double target, double dt, double omega) { double d = x - target, c = v + omega * d, e = Math.Exp(-omega * dt); x = target + (d + c * dt) * e; v = (v - omega * c * dt) * e; } // --- Форма: купол, тело, волнистый низ и подвижные ручки. static double Ball(double x, double y, double cx, double cy, double rx, double ry, double rz) { double q = 1 - Sq((x - cx) / rx) - Sq((y - cy) / ry); return q > 0 ? Math.Sqrt(q) * rz : -1; } static double Tube(double x, double y, double ax, double ay, double bx, double by, double r) { double dx = bx - ax, dy = by - ay, u = Clamp(((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy)); return Ball(x, y, ax + u * dx, ay + u * dy, r, r, r * .9); } static double HemY(double x) { return .76 + .19 * Math.Pow(Math.Abs(Math.Sin(x * 8.6 + .5)), .55); } // округлые капли, острые вырезы static double BodyZ(double x, double y) { if (y <= -1) return -1; double w; if (y < -.05) { double t = (y + .05) / .95; w = .72 * Math.Sqrt(Math.Max(0, 1 - t * t)); } else w = .72 + .05 * (y + .05); double yb = HemY(x); if (Math.Abs(x) >= w || y >= yb) return -1; double taper = y > -.05 ? 1 - .3 * Sq(y + .05) : 1; double z = Math.Sqrt(1 - Sq(x / w)) * Math.Min(w, .72) * 1.1 * taper * Math.Pow(Clamp((yb - y) / .12), .35); return z > 0 ? z : -1; } // Ручки при щекотке и побеге: плечи ходят вверх-вниз, кисти машут с запаздыванием. void ArmSwing(int side, out double sh, out double ax, out double ay) { sh = 0; ax = 0; ay = 0; double L = motion[LAUGH] * (1 - motion[COVER]); if (L < .01 || Reduced || Paused) return; double T = time * speed, opp = side > 0 ? Math.PI : 0, ph; string mode = State == "escape" ? "run" : MODES[reaction]; switch (mode) { case "same": ph = T * 11; sh = Math.Sin(ph) * .07; ay = Math.Sin(ph - .6) * .14; ax = side * Math.Abs(Math.Sin(ph)) * .045; break; case "alt": ph = T * 9 + opp; sh = Math.Sin(ph) * .08; ay = Math.Sin(ph - .7) * .16; ax = side * Math.Sin(ph + .4) * .05; break; case "up": ph = T * 10; sh = -.05 - Math.Abs(Math.Sin(ph)) * .05; ay = -.2 - Math.Abs(Math.Sin(ph)) * .1 + Math.Sin(ph * 2 + side) * .035; ax = side * .05; break; case "shake": ph = T * 15; sh = Math.Sin(ph) * .035; ay = .1 + Math.Sin(ph + side) * .05; ax = -side * .04; break; case "run": ph = T * 12 + opp; sh = Math.Sin(ph) * .06; ay = Math.Sin(ph - .5) * .15; ax = side * Math.Cos(ph) * .04; break; } sh *= L; ax *= L; ay *= L; } void HandPoint(int side, out double x, out double y, out double sh) { bool still = Reduced || Paused; double hand = motion[HAND]; double wave = still ? 0 : Math.Sin(time * 5 + side * .65) * .045 * hand; x = side * (.84 + hand * .05); y = .04 - hand * .3 + wave; if (!still) { // ручки плывут вслед за телом с запаздыванием и чуть «дышат» в стороны y += Math.Sin(time * 1.45 + .35 + side * .5) * .04; x += side * Math.Sin(time * 1.1 + side * 1.3) * .018; if (State == "idle" && Mood == "idle" && side == 1) { double ph = time % 14, hi = Smooth((ph - 10.6) / .35) * (1 - Smooth((ph - 12.3) / .35)); // раз в 14 с машет правой y -= .3 * hi; x += Math.Sin(time * 13) * .045 * hi; } if (State == "idle" && Mood == "thinking") y += Math.Sin(time * 6 + side * 1.6) * .03; } double ax, ay; ArmSwing(side, out sh, out ax, out ay); x += ax; y += ay + sh; double c = motion[COVER] * .9; // закрывает лицо ладошками x += (faceX + side * .26 - x) * c; y += (-.24 + faceY - y) * c; } double HandsZ(double x, double y) { double z = -1; for (int i = 0; i < 2; i++) { int side = i == 0 ? -1 : 1; z = Math.Max(z, Tube(x, y, side * .62, .1 + hsh[i], hx[i], hy[i], .12)); z = Math.Max(z, Ball(x, y, hx[i], hy[i], .14, .135, .26 + motion[COVER] * .06)); } return z; } double Depth(double x, double y) { return Math.Max(BodyZ(x, y), HandsZ(x, y)); } double BodyBend(double y) { double u = Clamp((.9 - y) / 1.9); return motion[BEND] * u * u; } // Лицо поворачивается вместе с yaw: дальний глаз становится меньше. void PaintFace(double x, double y, int[] c, ref char gl, bool blink) { double fx = faceX, fy = faceY, laugh = motion[LAUGH]; bool calm = State == "idle"; for (int side = -1; side <= 1; side += 2) { double bx = fx + side * .41 * (1 - Math.Abs(fx) * .3), q = Sq((x - bx) / .13) + Sq((y - (-.1 + fy)) / .075); if (q < 1) { Mix(c, BLUSH, .85 - q * .35); gl = q < .5 ? 'o' : '*'; } } bool happy = laugh > .45 || (Mood == "success" && calm); for (int side = -1; side <= 1; side += 2) { double far = side * fx, rx = .125 * (1 - far * .45), ry = .158 * (1 - far * .3); double ex = x - (fx + side * .25 * (1 - Math.Abs(fx) * .35)), ey = y - (-.31 + fy); if (happy) { if (Math.Abs(ex) < rx * 1.05 && Math.Abs(ey - (-.02 + Math.Abs(ex) * .75)) < .032) { Set(c, EYE); gl = '@'; } } else if (blink) { if (Math.Abs(ex) < rx * 1.05 && Math.Abs(ey) < .024) { Set(c, EYE); gl = '='; } } else if (Sq(ex / rx) + Sq(ey / ry) < 1) { if (Mood == "verdict" && calm && ey < -.01) continue; // приопущенные веки Set(c, EYE); gl = '@'; double lx = ex + rx * .28, ly = ey + ry * .3 - motion[GAZE] * ry * .25; if (Sq(lx / (rx * .46)) + Sq(ly / (ry * .4)) < 1) { Set(c, EYE_HL); gl = '@'; } else if (Sq((ex - rx * .35) / (rx * .22)) + Sq((ey - ry * .4) / (ry * .18)) < 1) { Mix(c, EYE_HL, .55); gl = '*'; } } } double mx = x - (fx + .03), my = y - (-.13 + fy); if (laugh > .08) { if (Sq(mx / (.1 + .07 * laugh)) + Sq((my - .03) / (.03 + .09 * laugh)) < 1) { Set(c, my > .04 ? TONGUE : MOUTH); gl = '@'; } } else if (Mood == "verdict" && calm) { if (Math.Abs(mx) < .08 && Math.Abs(my - .01) < .02) { Set(c, MOUTH); gl = '-'; } } else { double wide = Mood == "success" ? .14 : .105, dx = mx / wide; if (Math.Abs(dx) < 1) { double lower = (Mood == "success" ? .12 : .085) * (1 - dx * dx); if (my > -.02 && my < lower) { Set(c, my > lower * .55 && Math.Abs(dx) < .6 ? TONGUE : MOUTH); gl = '@'; } } } } // --- Кадр public void Tick() { double now = Now(), gap = now - lastMs; lastMs = now; double dt = Math.Max(0, Math.Min(.12, gap / 1000)); if (!Paused) time += Math.Min(gap / 1000, .15); var target = new double[N]; bool hidden = TargetPose(now, target); for (int k = 0; k < N; k++) { if (!seeded || Reduced || gap > 2000) { motion[k] = target[k]; vel[k] = 0; } else Spring(ref motion[k], ref vel[k], target[k], dt, k == X ? 14 : 10); } seeded = true; faceX = Clamp(.16 + motion[YAW] * .28, -.32, .38); faceY = motion[GAZE] * .12; for (int i = 0; i < 2; i++) HandPoint(i == 0 ? -1 : 1, out hx[i], out hy[i], out hsh[i]); PoseX = motion[X]; PoseY = motion[Y]; PoseRoll = motion[ROLL]; PoseHidden = hidden; Render(); } void Render() { Array.Clear(buf, 0, buf.Length); if (!PoseHidden) { double bob = Paused || Reading ? 0 : Math.Sin(time * 1.45 + 1) * 3.5; bool blink = !Paused && motion[LAUGH] < .08 && time % 5.8 > 5.55; double scan = (time * 7) % (ROWS + 10) - 5; var c = new int[3]; for (int row = 0; row < ROWS; row++) for (int col = 0; col < COLS; col++) { double px = col * CW + CW / 2.0, py = row * CH + CH / 2.0; double y = (py - CY - bob) / S, x = (px - W / 2.0) / S - BodyBend(y); double zb = BodyZ(x, y), zh = HandsZ(x, y), z = Math.Max(zb, zh); if (z <= 0) continue; const double e = .012; double zx = Depth(x + e, y), zy = Depth(x, y + e); double nx = zx > 0 ? (z - zx) / e : 0, ny = zy > 0 ? (z - zy) / e : 0, nz = 1; double n = Math.Sqrt(nx * nx + ny * ny + nz * nz); nx /= n; ny /= n; nz /= n; double diffuse = Clamp((nx * LX + ny * LY + nz * LZ + .35) / 1.35); // мягкий «обёрнутый» свет, как у матового пластика double spec = Math.Pow(Math.Max(0, nx * -.3 + ny * -.42 + nz * .86), 22); double b = Clamp(.2 + diffuse * .74 + (Math.Sin(col * 71 + row * 117) * .5 + .5) * .05 + spec * .3); double kk = b * (RAMP.Length - 1); int i0 = Math.Min(RAMP.Length - 2, (int)Math.Floor(kk)); Set(c, RAMP[i0]); Mix(c, RAMP[i0 + 1], kk - i0); char gl = GLYPHS[Math.Min(GLYPHS.Length - 1, (int)Math.Floor(b * GLYPHS.Length))]; if (y > .45) Mix(c, WARM, Clamp((y - .45) / .5) * .35); // тёплый «подсвеченный» низ if (zb >= zh) PaintFace(x, y, c, ref gl, blink); if (Mood == "thinking" && State == "idle" && !Paused && Math.Abs(row - scan) < 1.2 && gl != '@') { Mix(c, SCAN, .45); gl = '+'; } FillCell(col * CW, row * CH, c, 51); // лёгкая подложка: тело не выглядит бледным Glyph(gl, px, py, c); } if (State == "idle" && (Mood == "thinking" || Mood == "success")) { for (int i = 0; i < 6; i++) { double a = i * 1.047 + (Paused ? 0 : time * (Mood == "thinking" ? .9 : .4)); char g = Mood == "success" ? (i % 2 == 1 ? '+' : '*') : '·'; Glyph(g, W / 2.0 + Math.Cos(a) * 205, CY + bob + Math.Sin(a) * 150, Mood == "success" ? SPARK_OK : SPARK_RUN); } } } Bitmap.WritePixels(new Int32Rect(0, 0, W, H), buf, W * 4, 0); } void Blend(int idx, int[] c, int a) { int d = buf[idx], inv = 255 - a; int na = a + ((d >> 24) & 255) * inv / 255; int nr = c[0] * a / 255 + ((d >> 16) & 255) * inv / 255; int ng = c[1] * a / 255 + ((d >> 8) & 255) * inv / 255; int nb = c[2] * a / 255 + (d & 255) * inv / 255; buf[idx] = (na << 24) | (nr << 16) | (ng << 8) | nb; } void FillCell(int x0, int y0, int[] c, int a) { for (int y = y0; y < y0 + CH && y < H; y++) for (int x = x0; x < x0 + CW && x < W; x++) if (x >= 0 && y >= 0) Blend(y * W + x, c, a); } void Glyph(char ch, double cx, double cy, int[] c) { byte[] m; if (!masks.TryGetValue(ch, out m)) return; int[] bb = bounds[ch]; int ox = (int)Math.Round(cx - MW / 2.0), oy = (int)Math.Round(cy - MH / 2.0); for (int j = bb[2]; j <= bb[3]; j++) { int yy = oy + j; if (yy < 0 || yy >= H) continue; for (int i = bb[0]; i <= bb[1]; i++) { int xx = ox + i; if (xx < 0 || xx >= W) continue; int a = m[j * MW + i]; if (a > 0) Blend(yy * W + xx, c, a); } } } } // Запрос к серверу Всёреша в своём потоке. PowerShell только читает поля Done/Status/Body/Error: // фоновые потоки PowerShell с опросом по таймеру падали в 5.1 с «Типы аргумента не совпадают» (0.3.0–0.3.2). public class VseReshHttp { volatile bool done; public bool Done { get { return done; } } public int Status; public string Body, Error, Kind; public double Seconds; DateTime started; public static VseReshHttp Start(string kind, string method, string url, byte[] body, string contentType, string bearer, string userAgent, int timeoutSec) { var r = new VseReshHttp(); r.Kind = kind; r.started = DateTime.UtcNow; System.Threading.ThreadPool.QueueUserWorkItem(delegate { r.Run(method, url, body, contentType, bearer, userAgent, timeoutSec); }); return r; } void Run(string method, string url, byte[] body, string contentType, string bearer, string userAgent, int timeoutSec) { try { var request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); request.Method = method; request.UserAgent = userAgent; request.Timeout = timeoutSec * 1000; request.ReadWriteTimeout = timeoutSec * 1000; if (!string.IsNullOrEmpty(bearer)) request.Headers["Authorization"] = "Bearer " + bearer; if (body != null) { request.ContentType = contentType; request.ContentLength = body.Length; using (var stream = request.GetRequestStream()) stream.Write(body, 0, body.Length); } else if (method != "GET") request.ContentLength = 0; System.Net.HttpWebResponse response; try { response = (System.Net.HttpWebResponse)request.GetResponse(); } catch (System.Net.WebException e) { // Ответы 4xx/5xx — тоже ответы: в теле сервер объясняет, что не так. response = e.Response as System.Net.HttpWebResponse; if (response == null) throw; } using (response) using (var reader = new System.IO.StreamReader(response.GetResponseStream(), System.Text.Encoding.UTF8)) { Status = (int)response.StatusCode; Body = reader.ReadToEnd(); } } catch (Exception e) { while (e.InnerException != null) e = e.InnerException; Error = e.Message; } finally { Seconds = (DateTime.UtcNow - started).TotalSeconds; done = true; } } } '@ if (-not ('VseReshGhost' -as [type])) { $refs = @( [System.Windows.Media.Visual].Assembly.Location, # PresentationCore [System.Windows.DependencyObject].Assembly.Location, # WindowsBase [System.Xaml.XamlSchemaContext].Assembly.Location, # System.Xaml [System.Diagnostics.Stopwatch].Assembly.Location # System ) try { Add-Type -TypeDefinition $ghostSource -ReferencedAssemblies $refs -IgnoreWarnings } catch { Write-Host 'Не удалось собрать модуль отрисовки:' -ForegroundColor Red Write-Host $_ return } } # --------------------------------------------------------------------------- # Общие функции. Выполняются и в главном потоке, и в каждом фоновом сборщике. $VsHelpers = { $VsGenericNames = @('user', 'admin', 'administrator', 'администратор', 'пользователь', 'guest', 'owner', 'home', 'pc', 'default') $VsSharedProfiles = @('Public', 'Default', 'Default User', 'All Users', 'Общие') $VsServiceAccounts = @('SYSTEM', 'LOCAL SERVICE', 'NETWORK SERVICE', 'LocalSystem', 'LocalService', 'NetworkService', 'Administrators', 'Администраторы', 'Users', 'Пользователи', 'INTERACTIVE') # Другие пользователи компьютера тоже не должны быть узнаваемы: вместо имени — метка, # одинаковая для одного и того же человека в пределах этой установки. function Get-VsUserToken([string]$name) { if (-not $name -or $name.StartsWith('%') -or $VsSharedProfiles -contains $name) { return $name } if ($env:USERNAME -and $name -eq $env:USERNAME) { return '%USERNAME%' } '%USER-' + (Get-VsHash $name).Substring(0, 6) + '%' } # Замена совпадений без делегатов (scriptblock как MatchEvaluator в PowerShell 5.1 ненадёжен). function Protect-VsProfiles([string]$text, [string]$pattern) { $found = [regex]::Matches($text, $pattern) for ($i = $found.Count - 1; $i -ge 0; $i--) { $m = $found[$i] $text = $text.Substring(0, $m.Index) + (Get-VsUserToken $m.Value) + $text.Substring($m.Index + $m.Length) } $text } # Имена пользователей, имена компьютера (в том числе старые), пути профилей и SID не должны попадать в отчёт. function Protect-VsText([string]$text) { if (-not $text) { return $text } if ($env:USERPROFILE) { $text = $text -replace [regex]::Escape($env:USERPROFILE), '%USERPROFILE%' } $text = Protect-VsProfiles $text '(?i)(?<=[A-Za-z]:\\Users\\)(?!%)[^\\/"<>|:*?\r\n]+' $text = $text -replace 'S-1-5-21-\d+-\d+-\d+(-\d+)?', '%SID%' foreach ($pair in @(@($env:USERNAME, '%USERNAME%'), @($env:COMPUTERNAME, '%COMPUTERNAME%'))) { $value = $pair[0] if ($value -and $value.Length -ge 3 -and $VsGenericNames -notcontains $value) { $text = $text -replace ('(?[^\\]*)\\(?[^\\]+)$') { $domain = $Matches['domain'] $name = $Matches['name'] if ($domain -match '^NT (AUTHORITY|SERVICE)$' -or $VsServiceAccounts -contains $name) { return $account } if ($name.EndsWith('$')) { return ('{0}\%MACHINE%' -f $domain) } return ('{0}\{1}' -f $domain, (Get-VsUserToken $name)) } $account } # Повторная зачистка готового JSON: там обратные слэши удвоены. function Protect-VsJson([string]$json) { if ($env:USERPROFILE) { $json = $json -replace [regex]::Escape($env:USERPROFILE.Replace('\', '\\')), '%USERPROFILE%' } $json = Protect-VsProfiles $json '(?i)(?<=[A-Za-z]:\\\\Users\\\\)(?!%)[^\\"]+' Protect-VsText $json } # MAC-адреса и подобное: стабильно для этого компьютера, но не сопоставимо между компьютерами. function Get-VsHash([string]$value) { if (-not $value) { return $null } $hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($VsSalt)) $bytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($value.ToUpperInvariant())) ($bytes[0..7] | ForEach-Object { $_.ToString('x2') }) -join '' } # Выполняет кусок сбора; при ошибке пишет понятную заметку и продолжает. function Invoke-VsSafe([string]$what, [scriptblock]$action) { try { & $action } catch { $err = $_.Exception $msg = ("$($err.Message)" -replace '\s+', ' ').Trim() if ($err -is [UnauthorizedAccessException] -or $msg -match 'denied|Отказано в доступе|Доступ запрещен|доступ к ресурсу CIM|access the CIM resource|0x80041003|0x80070005') { $VsNotes.Add(('{0} — нужны права администратора' -f $what)) } else { if ($msg.Length -gt 180) { $msg = $msg.Substring(0, 180) + '…' } # Строка кода, на которой упало, — чтобы по отчёту было понятно, что чинить. $line = '' if ($_.InvocationInfo -and $_.InvocationInfo.Line) { $line = ($_.InvocationInfo.Line.Trim() -replace '\s+', ' ') } if ($line.Length -gt 140) { $line = $line.Substring(0, 140) + '…' } if ($line) { $msg += (' [в строке: {0}]' -f $line) } $VsNotes.Add(('{0} — не удалось: {1}' -f $what, (Protect-VsText $msg))) } } } function Get-VsReg([string]$path, [string]$name) { try { (Get-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop).$name } catch { $null } } # Путь к исполняемому файлу из командной строки (без аргументов). function Get-VsExe([string]$command) { if (-not $command) { return $null } $c = [Environment]::ExpandEnvironmentVariables($command.Trim()) if ($c -match '^"([^"]+)"') { return $matches[1] } if ($c -match '^(.+?\.(exe|com|bat|cmd|ps1|vbs|js|msc|scr|lnk|dll))(\s|,|$)') { return $matches[1] } ($c -split '\s+')[0] } # Запуск системной утилиты со скрытым окном и чтением вывода. function Invoke-VsNative([string]$file, [string]$arguments, [int]$timeoutSec = 20) { $psi = [System.Diagnostics.ProcessStartInfo]::new($file, $arguments) $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $oem = [Text.Encoding]::GetEncoding([Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage) $psi.StandardOutputEncoding = $oem $psi.StandardErrorEncoding = $oem $p = [System.Diagnostics.Process]::Start($psi) $out = $p.StandardOutput.ReadToEndAsync() $err = $p.StandardError.ReadToEndAsync() if (-not $p.WaitForExit($timeoutSec * 1000)) { try { $p.Kill() } catch { } throw ('{0} не завершился за {1} с' -f $file, $timeoutSec) } [pscustomobject]@{ ExitCode = $p.ExitCode; Output = $out.Result; Error = $err.Result } } # Размер папки с ограничением по времени; недоступные подпапки пропускаются. function Measure-VsFolder([string]$kind, [string]$path, [double]$maxSeconds = 8) { if (-not $path -or -not (Test-Path -LiteralPath $path)) { return $null } $sw = [Diagnostics.Stopwatch]::StartNew() $size = [long]0; $files = 0; $denied = 0; $complete = $true $stack = New-Object 'System.Collections.Generic.Stack[string]' $stack.Push($path) while ($stack.Count) { if ($sw.Elapsed.TotalSeconds -gt $maxSeconds) { $complete = $false; break } $dir = $stack.Pop() try { $di = [IO.DirectoryInfo]::new($dir) foreach ($f in $di.EnumerateFiles()) { $size += $f.Length; $files++ } foreach ($d in $di.EnumerateDirectories()) { if (-not ($d.Attributes -band [IO.FileAttributes]::ReparsePoint)) { $stack.Push($d.FullName) } } } catch { $denied++ } } [ordered]@{ kind = $kind; path = Protect-VsText $path; sizeMB = [math]::Round($size / 1MB, 1); files = $files; complete = $complete; deniedFolders = $denied } } # Событие журнала → простые значения. У EventLogRecord свойства Level и TimeCreated имеют # Nullable-тип, и любые сравнения и приведения прямо над ними в PowerShell 5.1 падают # с «Типы аргумента не совпадают». Поэтому сначала копируем значения в переменные. function Get-VsEventInfo($record) { $time = $record.TimeCreated $level = $record.Level $levelValue = $null if ($null -ne $level) { $levelValue = [int]"$level" } [ordered]@{ provider = "$($record.ProviderName)"; id = [int]"$($record.Id)"; level = $levelValue; time = $time } } # Чтение журнала событий без форматирования каждого сообщения (это медленно). function Read-VsEvents([string]$log, [string]$xpath, [int]$max = 3000) { $query = [System.Diagnostics.Eventing.Reader.EventLogQuery]::new($log, [System.Diagnostics.Eventing.Reader.PathType]::LogName, $xpath) $query.ReverseDirection = $true $reader = [System.Diagnostics.Eventing.Reader.EventLogReader]::new($query) $list = New-Object 'System.Collections.Generic.List[object]' try { while ($list.Count -lt $max) { $record = $null try { $record = $reader.ReadEvent() } catch { break } if ($null -eq $record) { break } $list.Add($record) } } finally { $reader.Dispose() } , $list } function Format-VsGB($bytes) { if ($null -eq $bytes -or "$bytes" -eq '') { return $null } [math]::Round([double]$bytes / 1GB, 1) } function Format-VsUptime([double]$hours) { if ($hours -lt 48) { '{0:N0} ч' -f $hours } else { '{0:N0} дн.' -f ($hours / 24) } } # Приводит данные к простым типам: даты — в ISO-строки, перечисления — в строки, # CIM-объекты — в словари. Так JSON получается одинаковым на любой системе. function ConvertTo-VsSafe($value, [int]$depth = 0) { if ($null -eq $value) { return $null } if ($depth -gt 12) { return "$value" } if ($value -is [string]) { return $value } if ($value -is [char]) { return "$value" } if ($value -is [bool]) { return $value } if ($value -is [datetime]) { return $value.ToString('yyyy-MM-ddTHH:mm:ssK') } if ($value -is [timespan]) { return [math]::Round($value.TotalSeconds, 1) } if ($value -is [enum]) { return $value.ToString() } if ($value -is [ValueType]) { return $value } if ($value -is [System.Collections.IDictionary]) { $h = [ordered]@{} foreach ($k in @($value.Keys)) { $h[[string]$k] = ConvertTo-VsSafe $value[$k] ($depth + 1) } return $h } if ($value -is [System.Collections.IEnumerable]) { $items = New-Object 'System.Collections.Generic.List[object]' foreach ($i in $value) { $items.Add((ConvertTo-VsSafe $i ($depth + 1))) } return , $items.ToArray() } if ($value.GetType().FullName -eq 'Microsoft.Management.Infrastructure.CimInstance') { $h = [ordered]@{} foreach ($p in $value.CimInstanceProperties) { $h[$p.Name] = ConvertTo-VsSafe $p.Value ($depth + 1) } return $h } if ($value -is [System.Management.Automation.PSCustomObject]) { $h = [ordered]@{} foreach ($p in $value.PSObject.Properties) { $h[$p.Name] = ConvertTo-VsSafe $p.Value ($depth + 1) } return $h } "$value" } } # --------------------------------------------------------------------------- # Сборщики. Каждый работает в своём фоновом потоке и возвращает @{ Summary; Data }. # Всё — только чтение и без прав администратора; чего не хватает — попадает в заметки. $collectors = @( @{ Id = 'perf'; Title = 'Нагрузка прямо сейчас'; Timeout = 30; Exclusive = $true Body = { $data = [ordered]@{} $logical = [Environment]::ProcessorCount $cpuSamples = New-Object 'System.Collections.Generic.List[int]' Invoke-VsSafe 'Загрузка процессора' { for ($i = 0; $i -lt 6; $i++) { $p = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'" if ($i -gt 0) { $cpuSamples.Add([int]$p.PercentProcessorTime) } Start-Sleep -Milliseconds 700 } } $data.cpuPercent = if ($cpuSamples.Count) { [math]::Round(($cpuSamples | Measure-Object -Average).Average) } else { $null } $data.cpuSamples = @($cpuSamples) $data.memory = Invoke-VsSafe 'Память' { $m = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Memory [ordered]@{ availableMB = [int64]$m.AvailableMBytes committedMB = [math]::Round($m.CommittedBytes / 1MB) commitLimitMB = [math]::Round($m.CommitLimit / 1MB) commitPercent = [int]$m.PercentCommittedBytesInUse pagesPerSec = [int64]$m.PagesPersec cacheMB = [math]::Round($m.CacheBytes / 1MB) } } $data.disks = @(Invoke-VsSafe 'Нагрузка на диски' { Get-CimInstance -ClassName Win32_PerfFormattedData_PerfDisk_PhysicalDisk | Where-Object { $_.Name -ne '_Total' } | ForEach-Object { [ordered]@{ name = $_.Name; busyPercent = [int]$_.PercentDiskTime; queue = [double]$_.AvgDiskQueueLength; bytesPerSec = [int64]$_.DiskBytesPersec } } }) $data.processes = Invoke-VsSafe 'Процессы' { $null = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process Start-Sleep -Milliseconds 800 $rows = @(Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process | Where-Object { $_.Name -ne '_Total' -and $_.Name -ne 'Idle' }) $byName = @{} foreach ($r in $rows) { $name = $r.Name -replace '#\d+$', '' if (-not $byName.ContainsKey($name)) { $byName[$name] = [ordered]@{ name = $name; instances = 0; cpuPercent = 0.0; memoryMB = 0.0 } } $entry = $byName[$name] $entry['instances'] += 1 $entry['cpuPercent'] += [double]$r.PercentProcessorTime / $logical $entry['memoryMB'] += [double]$r.WorkingSetPrivate / 1MB } $all = @($byName.Values) foreach ($entry in $all) { $entry['cpuPercent'] = [math]::Round($entry['cpuPercent'], 1); $entry['memoryMB'] = [math]::Round($entry['memoryMB']) } [ordered]@{ count = $rows.Count topCpu = @($all | Sort-Object { $_['cpuPercent'] } -Descending | Select-Object -First 15) topMemory = @($all | Sort-Object { $_['memoryMB'] } -Descending | Select-Object -First 15) } } $data.note = 'Замер сделан во время работы Всёреша: он сам немного нагружает процессор.' $parts = @() if ($null -ne $data.cpuPercent) { $parts += ('процессор загружен на {0}%' -f $data.cpuPercent) } if ($data.memory) { $parts += ('свободно {0:N1} ГБ памяти' -f ($data.memory['availableMB'] / 1024)) } @{ Summary = ($parts -join ', '); Data = $data } } } @{ Id = 'events'; Title = 'Журналы ошибок и сбоев'; Timeout = 120 Body = { $data = [ordered]@{} $day = [int64]86400000 # Ошибки за 14 дней, сгруппированные по источнику и коду, с одним примером текста. foreach ($log in 'System', 'Application') { $xp = '*[System[(Level=1 or Level=2) and TimeCreated[timediff(@SystemTime) <= {0}]]]' -f (14 * $day) $data[('errors14d' + $log)] = @(Invoke-VsSafe ('Ошибки в журнале {0}' -f $log) { $groups = @{} foreach ($record in (Read-VsEvents $log $xp 4000)) { $ev = Get-VsEventInfo $record $key = '{0}|{1}' -f $ev['provider'], $ev['id'] $g = $groups[$key] if ($null -eq $g) { $g = [ordered]@{ provider = $ev['provider']; id = $ev['id']; level = $ev['level']; hits = 0; first = $ev['time']; last = $ev['time']; sample = $null } $m = $null try { $m = $record.FormatDescription() } catch { } if (-not $m) { # У части источников нет текстов сообщений — берём хотя бы параметры события. $values = @($record.Properties | ForEach-Object { "$($_.Value)" } | Where-Object { $_ } | Select-Object -First 4) if ($values.Count) { $m = 'параметры: ' + ($values -join ' | ') } } if ($m) { $m = Protect-VsText (("$m" -replace '\s+', ' ').Trim()) if ($m.Length -gt 400) { $m = $m.Substring(0, 400) + '…' } $g['sample'] = $m } $groups[$key] = $g } $g['hits'] += 1 $t = $ev['time'] if ($null -ne $t) { if ($null -eq $g['first'] -or $t -lt $g['first']) { $g['first'] = $t } if ($null -eq $g['last'] -or $t -gt $g['last']) { $g['last'] = $t } } $record.Dispose() } $groups.Values | Sort-Object { $_['hits'] } -Descending | Select-Object -First 60 }) } # Критичные события за 90 дней: внезапные выключения, синие экраны, аппаратные и дисковые ошибки. $critical = New-Object 'System.Collections.Generic.List[object]' $specs = @( @{ Provider = 'Microsoft-Windows-Kernel-Power'; Ids = @(41) }, @{ Provider = 'EventLog'; Ids = @(6008) }, @{ Provider = 'Microsoft-Windows-WER-SystemErrorReporting'; Ids = @(1001) }, @{ Provider = 'Microsoft-Windows-WHEA-Logger'; Ids = @() }, @{ Provider = 'disk'; Ids = @(7, 51, 153) }, @{ Provider = 'Microsoft-Windows-Ntfs'; Ids = @(55, 98, 137) }, @{ Provider = 'Ntfs'; Ids = @(55, 98, 137) }, @{ Provider = 'volmgr'; Ids = @(161, 162) } ) foreach ($s in $specs) { $idPart = '' if ($s.Ids.Count) { $idPart = ' and (' + (($s.Ids | ForEach-Object { 'EventID={0}' -f $_ }) -join ' or ') + ')' } # Только предупреждения и ошибки: например, Ntfs 98 с уровнем «Сведения» означает «том исправен». $xp = "*[System[Provider[@Name='{0}']{1} and (Level=1 or Level=2 or Level=3) and TimeCreated[timediff(@SystemTime) <= {2}]]]" -f $s.Provider, $idPart, (90 * $day) Invoke-VsSafe ('События {0}' -f $s.Provider) { foreach ($record in (Read-VsEvents 'System' $xp 200)) { $ev = Get-VsEventInfo $record $detail = $null try { $firstValue = "$($record.Properties[0].Value)" if ($ev['id'] -eq 1001 -and $ev['provider'] -like '*WER-SystemErrorReporting') { $detail = $firstValue } elseif ($ev['id'] -eq 41 -and $ev['provider'] -like '*Kernel-Power') { $detail = '0x{0:X}' -f [int64]$firstValue } } catch { } $ev['detail'] = $detail $critical.Add($ev) $record.Dispose() } } } $data.critical90d = @($critical | Sort-Object { $_['time'] } -Descending) # Падения и зависания программ за 30 дней. $crashes = @{} $appSpecs = @(@{ Provider = 'Application Error'; Id = 1000 }, @{ Provider = 'Application Hang'; Id = 1002 }) foreach ($s in $appSpecs) { $xp = "*[System[Provider[@Name='{0}'] and EventID={1} and TimeCreated[timediff(@SystemTime) <= {2}]]]" -f $s.Provider, $s.Id, (30 * $day) Invoke-VsSafe ('Сбои программ ({0})' -f $s.Provider) { foreach ($record in (Read-VsEvents 'Application' $xp 2000)) { $ev = Get-VsEventInfo $record $values = @($record.Properties | ForEach-Object { "$($_.Value)" }) $isCrash = ($ev['id'] -eq 1000) $kind = 'hang' $module = '' $code = $null if ($isCrash) { $kind = 'crash' if ($values.Count -gt 3) { $module = $values[3] } if ($values.Count -gt 6) { $code = $values[6] } } $app = $null $version = $null if ($values.Count -gt 0) { $app = $values[0] } if ($values.Count -gt 1) { $version = $values[1] } $key = '{0}|{1}|{2}' -f $app, $module, $kind $c = $crashes[$key] if ($null -eq $c) { $c = [ordered]@{ app = $app; appVersion = $version; module = $module; exceptionCode = $code; kind = $kind; hits = 0; last = $ev['time'] } $crashes[$key] = $c } $c['hits'] += 1 $t = $ev['time'] if ($null -ne $t -and ($null -eq $c['last'] -or $t -gt $c['last'])) { $c['last'] = $t } $record.Dispose() } } } $data.appCrashes30d = @($crashes.Values | Sort-Object { $_['hits'] } -Descending | Select-Object -First 40) $data.stability = Invoke-VsSafe 'Индекс стабильности' { $rows = @(Get-CimInstance -ClassName Win32_ReliabilityStabilityMetrics | Sort-Object TimeGenerated -Descending | Select-Object -First 720) if ($rows.Count) { $daily = $rows | Group-Object { $_.TimeGenerated.ToString('yyyy-MM-dd') } | Select-Object -First 30 | ForEach-Object { [ordered]@{ date = $_.Name; index = [math]::Round(($_.Group | Measure-Object -Property SystemStabilityIndex -Minimum).Minimum, 2) } } [ordered]@{ current = [math]::Round($rows[0].SystemStabilityIndex, 2); daily = @($daily) } } } $data.minidumps = @(Invoke-VsSafe 'Файлы аварийных дампов' { $dir = Join-Path $env:SystemRoot 'Minidump' if (Test-Path -LiteralPath $dir) { Get-ChildItem -LiteralPath $dir -Filter '*.dmp' -ErrorAction Stop | Sort-Object LastWriteTime -Descending | Select-Object -First 30 | ForEach-Object { [ordered]@{ name = $_.Name; time = $_.LastWriteTime; sizeKB = [math]::Round($_.Length / 1KB) } } } }) $data.fullDumpExists = Test-Path -LiteralPath (Join-Path $env:SystemRoot 'MEMORY.DMP') $groupsCount = @($data.errors14dSystem).Count + @($data.errors14dApplication).Count $bsod = @($data.critical90d | Where-Object { $_['id'] -eq 1001 }).Count $summary = 'групп ошибок за 14 дней: {0}, критичных событий за 90 дней: {1}' -f $groupsCount, $data.critical90d.Count if ($bsod) { $summary += (', синих экранов: {0}' -f $bsod) } if ($data.appCrashes30d.Count) { $summary += (', программ со сбоями: {0}' -f $data.appCrashes30d.Count) } @{ Summary = $summary; Data = $data } } } @{ Id = 'drivers'; Title = 'Драйверы'; Timeout = 90 Body = { $classes = 'DISPLAY', 'NET', 'MEDIA', 'SCSIADAPTER', 'HDC', 'BLUETOOTH', 'USB', 'SYSTEM', 'MONITOR', 'CAMERA', 'IMAGE' $filter = ($classes | ForEach-Object { "DeviceClass='{0}'" -f $_ }) -join ' OR ' $rows = @(Invoke-VsSafe 'Список драйверов' { Get-CimInstance -ClassName Win32_PnPSignedDriver -Filter $filter }) $list = @($rows | Where-Object { $_.DeviceName -and (($_.DeviceClass -in 'DISPLAY', 'NET') -or ($_.DriverProviderName -notmatch '^Microsoft')) } | ForEach-Object { [ordered]@{ device = $_.DeviceName; class = $_.DeviceClass; provider = $_.DriverProviderName; manufacturer = $_.Manufacturer version = $_.DriverVersion; date = $_.DriverDate; signed = $_.IsSigned; inf = $_.InfName hardwareId = (("$($_.HardWareID)" -split '\\') | Select-Object -First 2) -join '\' } } | Sort-Object { $_['class'] }, { $_['device'] }) @{ Summary = ('драйверов от производителей: {0}' -f $list.Count); Data = [ordered]@{ drivers = $list } } } } @{ Id = 'activation'; Title = 'Активация Windows и Office'; Timeout = 60 Body = { $statusNames = @{ 0 = 'не активирована'; 1 = 'активирована'; 2 = 'пробный период'; 3 = 'пробный период'; 4 = 'неподлинная'; 5 = 'режим уведомлений'; 6 = 'продлённый период' } $products = @(Invoke-VsSafe 'Лицензии' { Get-CimInstance -ClassName SoftwareLicensingProduct -Filter 'PartialProductKey IS NOT NULL' | ForEach-Object { [ordered]@{ application = $(if ($_.ApplicationID -eq '55c92734-d682-4d71-983e-d6ec3f16059f') { 'windows' } elseif ($_.ApplicationID -eq '0ff1ce15-a989-479d-af46-f275c6370663') { 'office' } else { "$($_.ApplicationID)" }) name = $_.Name; description = $_.Description; licenseFamily = $_.LicenseFamily licenseStatus = [int]$_.LicenseStatus; gracePeriodMinutes = [int64]$_.GracePeriodRemaining kmsMachine = $_.KeyManagementServiceMachine; kmsDiscovered = $_.DiscoveredKeyManagementServiceMachineName } } }) $win = $products | Where-Object { $_['application'] -eq 'windows' } | Select-Object -First 1 $summary = 'сведений о лицензии нет' if ($win) { $summary = 'Windows: ' + $statusNames[[int]$win['licenseStatus']] if ("$($win['description'])" -match 'KMS') { $summary += ' (через KMS)' } } $office = @($products | Where-Object { $_['application'] -eq 'office' }).Count if ($office) { $summary += (', лицензий Office: {0}' -f $office) } @{ Summary = $summary; Data = [ordered]@{ products = $products } } } } @{ Id = 'os'; Title = 'Сведения о Windows'; Timeout = 45 Body = { $data = [ordered]@{} $cv = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' $os = Invoke-VsSafe 'Win32_OperatingSystem' { Get-CimInstance -ClassName Win32_OperatingSystem } if ($os) { $data.caption = "$($os.Caption)".Trim() $data.version = $os.Version $data.build = [int]$os.BuildNumber $data.architecture = $os.OSArchitecture $data.installDate = $os.InstallDate $data.lastBoot = $os.LastBootUpTime $data.uptimeHours = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalHours, 1) $data.osLanguage = $os.OSLanguage $data.muiLanguages = @($os.MUILanguages) $data.totalMemoryMB = [math]::Round($os.TotalVisibleMemorySize / 1KB) $data.freeMemoryMB = [math]::Round($os.FreePhysicalMemory / 1KB) $data.systemDrive = $os.SystemDrive } $data.displayVersion = Get-VsReg $cv 'DisplayVersion' $data.releaseId = Get-VsReg $cv 'ReleaseId' $data.ubr = Get-VsReg $cv 'UBR' $data.editionId = Get-VsReg $cv 'EditionID' $data.installationType = Get-VsReg $cv 'InstallationType' $data.computer = Invoke-VsSafe 'Win32_ComputerSystem' { $cs = Get-CimInstance -ClassName Win32_ComputerSystem [ordered]@{ manufacturer = $cs.Manufacturer; model = $cs.Model; systemType = $cs.SystemType; pcSystemType = [int]$cs.PCSystemType totalMemoryGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1); partOfDomain = [bool]$cs.PartOfDomain hypervisorPresent = [bool]$cs.HypervisorPresent } } # PEFirmwareType: 1 — BIOS (Legacy), 2 — UEFI. # PEFirmwareType есть не везде. Confirm-SecureBootUEFI без прав отвечает «нет доступа» на UEFI # и «не поддерживается» на BIOS — этого достаточно, чтобы различить. $firmwareType = switch (Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control' 'PEFirmwareType') { 1 { 'BIOS' } 2 { 'UEFI' } default { $null } } if (-not $firmwareType) { try { $null = Confirm-SecureBootUEFI; $firmwareType = 'UEFI' } catch { if ($_.Exception -is [PlatformNotSupportedException] -or "$($_.Exception.Message)" -match 'not supported|не поддерживается') { $firmwareType = 'BIOS' } elseif ($_.Exception -is [UnauthorizedAccessException] -or "$($_.Exception.Message)" -match 'denied|доступ') { $firmwareType = 'UEFI' } } } $data.firmware = [ordered]@{ type = $firmwareType secureBoot = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State' 'UEFISecureBootEnabled' } $data.power = [ordered]@{ fastStartup = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' 'HiberbootEnabled' hibernateEnabled = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' 'HibernateEnabled' hibernateDefault = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' 'HibernateEnabledDefault' } $data.deviceGuard = Invoke-VsSafe 'VBS (защита на основе виртуализации)' { $dg = Get-CimInstance -Namespace 'root\Microsoft\Windows\DeviceGuard' -ClassName Win32_DeviceGuard [ordered]@{ vbsStatus = [int]$dg.VirtualizationBasedSecurityStatus servicesConfigured = @($dg.SecurityServicesConfigured | ForEach-Object { [int]$_ }) servicesRunning = @($dg.SecurityServicesRunning | ForEach-Object { [int]$_ }) } } $data.tpm = Invoke-VsSafe 'TPM' { $t = Get-CimInstance -Namespace 'root\CIMV2\Security\MicrosoftTpm' -ClassName Win32_Tpm if ($t) { [ordered]@{ present = $true; enabled = [bool]$t.IsEnabled_InitialValue; activated = [bool]$t.IsActivated_InitialValue; specVersion = $t.SpecVersion } } else { [ordered]@{ present = $false } } } $data.locale = [ordered]@{ culture = (Get-Culture).Name; uiCulture = (Get-UICulture).Name; timeZone = (Get-TimeZone).Id } $data.powershell = [ordered]@{ version = "$($PSVersionTable.PSVersion)"; edition = $PSVersionTable.PSEdition; is64BitProcess = [Environment]::Is64BitProcess } $data.dotnetRelease = Get-VsReg 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' 'Release' $name = if ($data.caption) { $data.caption -replace '^(Microsoft|Майкрософт)\s+', '' } else { 'Windows' } $ver = @($data.displayVersion, ('сборка {0}.{1}' -f $data.build, $data.ubr)) | Where-Object { $_ } $summary = '{0} {1}' -f $name, ($ver -join ', ') if ($null -ne $data.uptimeHours) { $summary += (', без перезагрузки {0}' -f (Format-VsUptime $data.uptimeHours)) } @{ Summary = $summary; Data = $data } } } @{ Id = 'hardware'; Title = 'Железо: процессор, память, диски, сеть'; Timeout = 75 Body = { $data = [ordered]@{} $data.cpu = @(Invoke-VsSafe 'Процессор' { Get-CimInstance -ClassName Win32_Processor | ForEach-Object { [ordered]@{ name = ("$($_.Name)" -replace '\s+', ' ').Trim(); manufacturer = $_.Manufacturer cores = [int]$_.NumberOfCores; threads = [int]$_.NumberOfLogicalProcessors maxClockMHz = [int]$_.MaxClockSpeed; currentClockMHz = [int]$_.CurrentClockSpeed l2KB = [int]$_.L2CacheSize; l3KB = [int]$_.L3CacheSize; socket = $_.SocketDesignation virtualizationFirmwareEnabled = $_.VirtualizationFirmwareEnabled } } }) # Номинальная (speed) и фактическая (configured) частота: так видно, что не включён XMP. $data.memory = Invoke-VsSafe 'Память' { $modules = @(Get-CimInstance -ClassName Win32_PhysicalMemory | ForEach-Object { [ordered]@{ sizeGB = [math]::Round($_.Capacity / 1GB, 1); speedMHz = [int]$_.Speed; configuredMHz = [int]$_.ConfiguredClockSpeed manufacturer = ("$($_.Manufacturer)" -replace '\s+', ' ').Trim(); partNumber = ("$($_.PartNumber)" -replace '\s+', ' ').Trim() slot = $_.DeviceLocator; bank = $_.BankLabel; formFactor = [int]$_.FormFactor; smbiosType = [int]$_.SMBIOSMemoryType } }) $array = Get-CimInstance -ClassName Win32_PhysicalMemoryArray | Select-Object -First 1 [ordered]@{ totalGB = [math]::Round((($modules | ForEach-Object { $_['sizeGB'] }) | Measure-Object -Sum).Sum, 1) modules = $modules slots = $(if ($array) { [int]$array.MemoryDevices } else { $null }) maxCapacityGB = $(if ($array -and $array.MaxCapacityEx) { [math]::Round($array.MaxCapacityEx / 1MB) } else { $null }) } } $data.board = Invoke-VsSafe 'Материнская плата' { $b = Get-CimInstance -ClassName Win32_BaseBoard | Select-Object -First 1 [ordered]@{ manufacturer = $b.Manufacturer; product = $b.Product; version = $b.Version } } $data.bios = Invoke-VsSafe 'BIOS' { $b = Get-CimInstance -ClassName Win32_BIOS | Select-Object -First 1 [ordered]@{ manufacturer = $b.Manufacturer; version = $b.SMBIOSBIOSVersion; releaseDate = $b.ReleaseDate; smbios = ('{0}.{1}' -f $b.SMBIOSMajorVersion, $b.SMBIOSMinorVersion) } } # Win32_VideoController не показывает больше 4 ГБ видеопамяти — настоящий объём берём из реестра. $vram = @{} Invoke-VsSafe 'Объём видеопамяти' { $class = 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}' foreach ($k in @(Get-ChildItem -LiteralPath $class -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match '^\d{4}$' })) { $desc = $k.GetValue('DriverDesc') $size = $k.GetValue('HardwareInformation.qwMemorySize') if ($size -is [byte[]] -and $size.Length -ge 8) { $size = [BitConverter]::ToInt64($size, 0) } if ($desc -and $size) { $vram["$desc"] = [int64]$size } } } $data.gpu = @(Invoke-VsSafe 'Видеокарта' { Get-CimInstance -ClassName Win32_VideoController | ForEach-Object { $ids = $null if ("$($_.PNPDeviceID)" -match '(VEN_[0-9A-Fa-f]{4}&DEV_[0-9A-Fa-f]{4})') { $ids = $matches[1] } $memoryMB = $null if ($vram.ContainsKey("$($_.Name)")) { $memoryMB = [math]::Round($vram["$($_.Name)"] / 1MB) } elseif ($_.AdapterRAM) { $memoryMB = [math]::Round([int64]$_.AdapterRAM / 1MB) } [ordered]@{ name = $_.Name; driverVersion = $_.DriverVersion; driverDate = $_.DriverDate; memoryMB = $memoryMB; pciIds = $ids resolution = $(if ($_.CurrentHorizontalResolution) { '{0}x{1}' -f $_.CurrentHorizontalResolution, $_.CurrentVerticalResolution } else { $null }) refreshRate = $_.CurrentRefreshRate; status = $_.Status } } }) $data.physicalDisks = @(Invoke-VsSafe 'Физические диски' { Get-PhysicalDisk | ForEach-Object { [ordered]@{ id = "$($_.DeviceId)"; model = ("$($_.FriendlyName)" -replace '\s+', ' ').Trim(); mediaType = "$($_.MediaType)"; busType = "$($_.BusType)" sizeGB = Format-VsGB $_.Size; health = "$($_.HealthStatus)"; operational = (@($_.OperationalStatus) -join ', ') spindleSpeed = $_.SpindleSpeed; firmware = $_.FirmwareVersion } } }) $data.diskReliability = @(Invoke-VsSafe 'Износ и ошибки дисков (SMART)' { Get-PhysicalDisk | ForEach-Object { $d = $_ $r = $d | Get-StorageReliabilityCounter [ordered]@{ id = "$($d.DeviceId)"; temperatureC = $r.Temperature; temperatureMaxC = $r.TemperatureMax; wearPercent = $r.Wear powerOnHours = $r.PowerOnHours; readErrorsUncorrected = $r.ReadErrorsUncorrected; writeErrorsUncorrected = $r.WriteErrorsUncorrected startStopCycles = $r.StartStopCycleCount } } }) $data.disks = @(Invoke-VsSafe 'Разметка дисков' { Get-Disk | ForEach-Object { [ordered]@{ number = $_.Number; model = $_.FriendlyName; partitionStyle = "$($_.PartitionStyle)"; isBoot = $_.IsBoot; isSystem = $_.IsSystem; sizeGB = Format-VsGB $_.Size; busType = "$($_.BusType)" } } }) $data.volumes = @(Invoke-VsSafe 'Разделы' { Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object { [ordered]@{ drive = $_.DeviceID; label = Protect-VsText $_.VolumeName; fileSystem = $_.FileSystem sizeGB = Format-VsGB $_.Size; freeGB = Format-VsGB $_.FreeSpace freePercent = $(if ($_.Size) { [math]::Round(100.0 * $_.FreeSpace / $_.Size) } else { $null }) } } }) # Состояние BitLocker без прав администратора — так, как его видит Проводник. Invoke-VsSafe 'Шифрование BitLocker' { $shell = New-Object -ComObject Shell.Application foreach ($v in $data.volumes) { $v['bitlockerRaw'] = $shell.NameSpace($v['drive'] + '\').Self.ExtendedProperty('System.Volume.BitLockerProtection') } } $batteries = @(Invoke-VsSafe 'Батарея' { Get-CimInstance -ClassName Win32_Battery }) if ($batteries.Count) { $data.battery = Invoke-VsSafe 'Отчёт о батарее' { $tmp = Join-Path $env:TEMP ('vseresh-battery-{0}.xml' -f [guid]::NewGuid().ToString('N')) try { $null = Invoke-VsNative 'powercfg.exe' ('/batteryreport /xml /output "{0}"' -f $tmp) 30 $x = New-Object System.Xml.XmlDocument $x.Load($tmp) [ordered]@{ chargePercent = ($batteries | Select-Object -First 1).EstimatedChargeRemaining batteries = @($x.BatteryReport.Batteries.Battery | ForEach-Object { $design = [int64]"$($_.DesignCapacity)"; $full = [int64]"$($_.FullChargeCapacity)" [ordered]@{ manufacturer = "$($_.Manufacturer)"; chemistry = "$($_.Chemistry)"; designCapacity = $design; fullChargeCapacity = $full cycleCount = "$($_.CycleCount)"; healthPercent = $(if ($design) { [math]::Round(100.0 * $full / $design) } else { $null }) } }) } } finally { Remove-Item -LiteralPath $tmp -ErrorAction SilentlyContinue } } } $data.monitors = @(Invoke-VsSafe 'Мониторы' { $decode = { param($codes) if ($codes) { (-join ($codes | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })).Trim() } } Get-CimInstance -Namespace 'root\wmi' -ClassName WmiMonitorID | ForEach-Object { [ordered]@{ manufacturer = & $decode $_.ManufacturerName; name = & $decode $_.UserFriendlyName; year = [int]$_.YearOfManufacture; active = [bool]$_.Active } } }) $data.network = @(Invoke-VsSafe 'Сетевые адаптеры' { $dns = @{} foreach ($d in @(Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue)) { if ($d.ServerAddresses) { $dns[[int]$d.InterfaceIndex] = @($d.ServerAddresses) } } # Имя сети (SSID) не берём — только тип сети и есть ли интернет. $profiles = @{} foreach ($p in @(Get-NetConnectionProfile -ErrorAction SilentlyContinue)) { $profiles[[int]$p.InterfaceIndex] = [ordered]@{ category = "$($p.NetworkCategory)"; ipv4 = "$($p.IPv4Connectivity)"; ipv6 = "$($p.IPv6Connectivity)" } } Get-NetAdapter -Physical | ForEach-Object { [ordered]@{ name = $_.Name; description = $_.InterfaceDescription; status = "$($_.Status)"; linkSpeed = $_.LinkSpeed mediaType = $_.PhysicalMediaType; driverVersion = $_.DriverVersionString; driverDate = $_.DriverDate mac = Get-VsHash $_.MacAddress; dns = $dns[[int]$_.ifIndex]; profile = $profiles[[int]$_.ifIndex] } } }) $data.problemDevices = @(Invoke-VsSafe 'Устройства с ошибками' { Get-CimInstance -ClassName Win32_PnPEntity -Filter 'ConfigManagerErrorCode <> 0' | ForEach-Object { [ordered]@{ name = $_.Name; class = $_.PNPClass; errorCode = [int]$_.ConfigManagerErrorCode; status = $_.Status hardwareId = (("$($_.PNPDeviceID)" -split '\\') | Select-Object -First 2) -join '\' } } }) $parts = @() $cpu = $data.cpu | Select-Object -First 1 if ($cpu) { $parts += $cpu['name'] } if ($data.memory) { $parts += ('{0:N0} ГБ памяти' -f $data.memory['totalGB']) } $sys = $data.volumes | Where-Object { $_['drive'] -eq $env:SystemDrive } | Select-Object -First 1 if ($sys) { $parts += ('на {0} свободно {1:N0} ГБ' -f $sys['drive'], $sys['freeGB']) } if ($data.problemDevices.Count) { $parts += ('устройств с ошибками: {0}' -f $data.problemDevices.Count) } @{ Summary = ($parts -join ', '); Data = $data } } } @{ Id = 'updates'; Title = 'Обновления Windows'; Timeout = 75 Body = { $data = [ordered]@{} $data.history = @(Invoke-VsSafe 'История Windows Update' { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $total = $searcher.GetTotalHistoryCount() if ($total -gt 0) { $resultNames = @{ 0 = 'notStarted'; 1 = 'inProgress'; 2 = 'succeeded'; 3 = 'succeededWithErrors'; 4 = 'failed'; 5 = 'aborted' } foreach ($h in $searcher.QueryHistory(0, [math]::Min($total, 80))) { if (-not $h.Title) { continue } [ordered]@{ date = $h.Date; title = $h.Title kb = $(if ($h.Title -match '(KB\d{6,8})') { $matches[1] } else { $null }) result = $resultNames[[int]$h.ResultCode]; operation = [int]$h.Operation; hresult = ('0x{0:X8}' -f $h.HResult) isDefinition = [bool]($h.Title -match 'KB2267602|KB2461484|KB915597|KB4052623') } } } }) $data.hotfixes = @(Invoke-VsSafe 'Установленные исправления' { Get-CimInstance -ClassName Win32_QuickFixEngineering | ForEach-Object { [ordered]@{ id = $_.HotFixID; description = $_.Description; installedOn = $_.InstalledOn } } }) $data.pendingReboot = [ordered]@{ componentServicing = Test-Path -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' windowsUpdate = Test-Path -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' fileRename = [bool](Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' 'PendingFileRenameOperations') } # Start: 2 — автоматически, 3 — вручную, 4 — отключена. $data.services = @(foreach ($name in 'wuauserv', 'BITS', 'UsoSvc', 'WaaSMedicSvc', 'TrustedInstaller', 'DoSvc') { $status = $null try { $status = "$((Get-Service -Name $name -ErrorAction Stop).Status)" } catch { } [ordered]@{ name = $name; status = $status; start = Get-VsReg ('HKLM:\SYSTEM\CurrentControlSet\Services\' + $name) 'Start' } }) $wu = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' $data.policies = [ordered]@{ noAutoUpdate = Get-VsReg ($wu + '\AU') 'NoAutoUpdate' auOptions = Get-VsReg ($wu + '\AU') 'AUOptions' wsus = [bool](Get-VsReg $wu 'WUServer') targetReleaseVersion = Get-VsReg $wu 'TargetReleaseVersionInfo' deferFeatureDays = Get-VsReg $wu 'DeferFeatureUpdatesPeriodInDays' pauseUpdatesExpiry = Get-VsReg 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' 'PauseUpdatesExpiryTime' } $installs = @($data.history | Where-Object { $_['result'] -eq 'succeeded' -and -not $_['isDefinition'] -and $_['operation'] -eq 1 }) $last = $installs | Sort-Object { $_['date'] } -Descending | Select-Object -First 1 $failed = @($data.history | Where-Object { $_['result'] -eq 'failed' }).Count $parts = @() if ($last) { $parts += ('последнее обновление {0}' -f $last['date'].ToLocalTime().ToString('dd.MM.yyyy')) } else { $parts += 'история обновлений пуста' } # PendingFileRenameOperations оставляют многие установщики — это не признак обновления. if ($data.pendingReboot['componentServicing'] -or $data.pendingReboot['windowsUpdate']) { $parts += 'обновление ждёт перезагрузки' } if ($failed) { $parts += ('неудачных установок: {0}' -f $failed) } @{ Summary = ($parts -join ', '); Data = $data } } } @{ Id = 'software'; Title = 'Установленные программы'; Timeout = 75 Body = { $data = [ordered]@{} $items = @{} $roots = @( @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'; Scope = 'machine' }, @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'; Scope = 'machine32' }, @{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall'; Scope = 'user' } ) foreach ($root in $roots) { if (-not (Test-Path -LiteralPath $root.Path)) { continue } Invoke-VsSafe ('Список программ ({0})' -f $root.Scope) { foreach ($key in @(Get-ChildItem -LiteralPath $root.Path -ErrorAction SilentlyContinue)) { $name = $key.GetValue('DisplayName') if (-not $name) { continue } if ($key.GetValue('ParentKeyName') -or ($key.GetValue('ReleaseType') -in 'Update', 'Hotfix', 'Security Update')) { continue } $version = $key.GetValue('DisplayVersion') $id = '{0}|{1}' -f $name, $version if ($items.ContainsKey($id)) { continue } $size = $key.GetValue('EstimatedSize') $items[$id] = [ordered]@{ name = "$name".Trim(); version = $version; publisher = $key.GetValue('Publisher'); installDate = $key.GetValue('InstallDate') sizeMB = $(if ($size) { [math]::Round([double]$size / 1024) } else { $null }); scope = $root.Scope systemComponent = ($key.GetValue('SystemComponent') -eq 1) } } } } $data.programs = @($items.Values | Sort-Object { $_['name'] }) $data.storeApps = @(Invoke-VsSafe 'Приложения Microsoft Store' { Get-AppxPackage | Where-Object { -not $_.IsFramework -and "$($_.SignatureKind)" -ne 'System' } | ForEach-Object { [ordered]@{ name = $_.Name; version = "$($_.Version)"; signature = "$($_.SignatureKind)"; publisher = ("$($_.Publisher)" -replace '^CN=([^,]+).*$', '$1') } } }) # Версии браузеров — по исполняемым файлам. Профили и данные браузеров не трогаем. $pf = $env:ProgramFiles; $pf86 = ${env:ProgramFiles(x86)}; $la = $env:LOCALAPPDATA $browserPaths = [ordered]@{ 'Google Chrome' = @("$pf\Google\Chrome\Application\chrome.exe", "$pf86\Google\Chrome\Application\chrome.exe", "$la\Google\Chrome\Application\chrome.exe") 'Microsoft Edge' = @("$pf86\Microsoft\Edge\Application\msedge.exe", "$pf\Microsoft\Edge\Application\msedge.exe") 'Mozilla Firefox' = @("$pf\Mozilla Firefox\firefox.exe", "$pf86\Mozilla Firefox\firefox.exe") 'Яндекс Браузер' = @("$la\Yandex\YandexBrowser\Application\browser.exe", "$pf\Yandex\YandexBrowser\Application\browser.exe", "$pf86\Yandex\YandexBrowser\Application\browser.exe") 'Opera' = @("$la\Programs\Opera\opera.exe") 'Opera GX' = @("$la\Programs\Opera GX\opera.exe") 'Brave' = @("$pf\BraveSoftware\Brave-Browser\Application\brave.exe", "$la\BraveSoftware\Brave-Browser\Application\brave.exe") 'Vivaldi' = @("$la\Vivaldi\Application\vivaldi.exe", "$pf\Vivaldi\Application\vivaldi.exe") } $data.browsers = @(Invoke-VsSafe 'Браузеры' { foreach ($b in $browserPaths.Keys) { foreach ($p in $browserPaths[$b]) { if ($p -notmatch '^[A-Za-z]:\\') { continue } if (Test-Path -LiteralPath $p) { [ordered]@{ name = $b; version = (Get-Item -LiteralPath $p).VersionInfo.ProductVersion; path = Protect-VsText $p } break } } } }) $data.defaultBrowser = Get-VsReg 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice' 'ProgId' @{ Summary = ('программ: {0}, приложений Store: {1}, браузеров: {2}' -f $data.programs.Count, $data.storeApps.Count, $data.browsers.Count); Data = $data } } } @{ Id = 'startup'; Title = 'Автозагрузка, службы и быстродействие'; Timeout = 90 Body = { $data = [ordered]@{} # Что пользователь отключил в «Диспетчере задач → Автозагрузка»: нечётный первый байт — выключено. $approved = @{} foreach ($hive in 'HKCU', 'HKLM') { foreach ($sub in 'Run', 'Run32', 'StartupFolder') { $path = '{0}:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\{1}' -f $hive, $sub if (-not (Test-Path -LiteralPath $path)) { continue } try { $key = Get-Item -LiteralPath $path foreach ($valueName in $key.GetValueNames()) { $bytes = $key.GetValue($valueName) if ($bytes -is [byte[]] -and $bytes.Length) { $approved[$valueName] = (($bytes[0] -band 1) -eq 0) } } } catch { } } } # Для скриптовых хостов аргументы важны (через них часто запускается вредное) — сохраняем их. $scriptHosts = 'powershell', 'pwsh', 'cmd', 'wscript', 'cscript', 'mshta', 'rundll32', 'regsvr32', 'conhost' $describeCommand = { param($command) $raw = Get-VsExe $command $leaf = ("$raw" -replace '^.*\\', '' -replace '\.\w+$', '').ToLowerInvariant() $full = $null if ($scriptHosts -contains $leaf) { $full = Protect-VsText "$command" if ($full.Length -gt 300) { $full = $full.Substring(0, 300) + '…' } } $exists = $null if ($raw -match '^[A-Za-z]:\\') { $exists = Test-Path -LiteralPath $raw } # Человеческое название и производитель — из описания самого файла. $product = $null; $company = $null if ($exists) { try { $vi = [Diagnostics.FileVersionInfo]::GetVersionInfo($raw) $product = "$($vi.FileDescription)".Trim() if (-not $product) { $product = "$($vi.ProductName)".Trim() } $company = "$($vi.CompanyName)".Trim() } catch { } } [ordered]@{ executable = Protect-VsText $raw; command = $full; exists = $exists; product = Protect-VsText $product; company = Protect-VsText $company } } $data.startupItems = @(Invoke-VsSafe 'Автозагрузка' { Get-CimInstance -ClassName Win32_StartupCommand | ForEach-Object { $d = & $describeCommand $_.Command [ordered]@{ name = Protect-VsText $_.Name; product = $d['product']; company = $d['company'] executable = $d['executable']; command = $d['command']; exists = $d['exists'] location = Protect-VsText $_.Location; enabled = $(if ($approved.ContainsKey($_.Name)) { $approved[$_.Name] } else { $true }) } } }) $data.scheduledTasks = @(Invoke-VsSafe 'Планировщик заданий' { Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' } | Select-Object -First 200 | ForEach-Object { $task = $_ $action = @($task.Actions) | Select-Object -First 1 $d = $null if ($action -and $action.Execute) { $d = & $describeCommand (('"{0}" {1}' -f $action.Execute.Trim('"'), $action.Arguments).Trim()) } [ordered]@{ name = Protect-VsText $task.TaskName; path = Protect-VsText $task.TaskPath; state = "$($task.State)"; author = Protect-VsAccount $task.Author triggers = @($task.Triggers | ForEach-Object { "$($_.CimClass.CimClassName)" -replace '^MSFT_Task', '' -replace 'Trigger$', '' }) hidden = [bool]$task.Settings.Hidden executable = $(if ($d) { $d['executable'] } else { $null }); command = $(if ($d) { $d['command'] } else { $null }); exists = $(if ($d) { $d['exists'] } else { $null }) product = $(if ($d) { $d['product'] } else { $null }); company = $(if ($d) { $d['company'] } else { $null }) } } }) # Все сторонние службы и все системные с автозапуском. $data.services = @(Invoke-VsSafe 'Службы' { Get-CimInstance -ClassName Win32_Service | ForEach-Object { $raw = Get-VsExe $_.PathName $thirdParty = [bool]($raw -and ($raw -notmatch '^[A-Za-z]:\\Windows\\')) if ($thirdParty -or $_.StartMode -eq 'Auto') { [ordered]@{ name = $_.Name; displayName = $_.DisplayName; startMode = $_.StartMode; state = $_.State; delayedAutoStart = $_.DelayedAutoStart thirdParty = $thirdParty; executable = Protect-VsText $raw; account = Protect-VsAccount $_.StartName } } } }) $data.pagefile = Invoke-VsSafe 'Файл подкачки' { $cs = Get-CimInstance -ClassName Win32_ComputerSystem [ordered]@{ automatic = [bool]$cs.AutomaticManagedPagefile settings = @(Get-CimInstance -ClassName Win32_PageFileSetting | ForEach-Object { [ordered]@{ path = $_.Name; initialMB = [int]$_.InitialSize; maximumMB = [int]$_.MaximumSize } }) usage = @(Get-CimInstance -ClassName Win32_PageFileUsage | ForEach-Object { [ordered]@{ path = $_.Name; allocatedMB = [int]$_.AllocatedBaseSize; currentMB = [int]$_.CurrentUsage; peakMB = [int]$_.PeakUsage } }) } } $mask = Get-VsReg 'HKCU:\Control Panel\Desktop' 'UserPreferencesMask' $data.visual = [ordered]@{ visualFxSetting = Get-VsReg 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects' 'VisualFXSetting' transparency = Get-VsReg 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' 'EnableTransparency' minAnimate = Get-VsReg 'HKCU:\Control Panel\Desktop\WindowMetrics' 'MinAnimate' userPreferencesMask = $(if ($mask -is [byte[]]) { ($mask | ForEach-Object { $_.ToString('x2') }) -join '' } else { $null }) } $data.powerPlan = Invoke-VsSafe 'Схема электропитания' { $r = Invoke-VsNative 'powercfg.exe' '/getactivescheme' 15 $guid = $null; $name = $null if ($r.Output -match '([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})') { $guid = $matches[1].ToLowerInvariant() } if ($r.Output -match '\(([^)]+)\)\s*$') { $name = $matches[1] } $known = @{ '381b4222-f694-41f0-9685-ff5bb260df2e' = 'balanced'; '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' = 'highPerformance' 'a1841308-3541-4fab-bc81-f71556f20b4a' = 'powerSaver'; 'e9a42b02-d5df-448d-aa00-03f14749eb61' = 'ultimatePerformance' } $schemes = 'HKLM:\SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes' [ordered]@{ guid = $guid; name = $name; kind = $(if ($guid -and $known.ContainsKey($guid)) { $known[$guid] } else { 'custom' }) overlayAc = Get-VsReg $schemes 'ActiveOverlayAcPowerScheme'; overlayDc = Get-VsReg $schemes 'ActiveOverlayDcPowerScheme' } } $data.gaming = [ordered]@{ gameMode = Get-VsReg 'HKCU:\Software\Microsoft\GameBar' 'AutoGameModeEnabled' gameDvr = Get-VsReg 'HKCU:\System\GameConfigStore' 'GameDVR_Enabled' hardwareScheduling = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers' 'HwSchMode' } $data.storageSense = Get-VsReg 'HKCU:\Software\Microsoft\Windows\CurrentVersion\StorageSense\Parameters\StoragePolicy' '01' $data.optimizeDrivesTask = Invoke-VsSafe 'Расписание оптимизации дисков' { "$((Get-ScheduledTask -TaskPath '\Microsoft\Windows\Defrag\' -TaskName 'ScheduledDefrag').State)" } # Только размеры служебных папок; личные папки не перебираем. $data.folders = @(@( Measure-VsFolder 'userTemp' $env:TEMP 10 Measure-VsFolder 'windowsTemp' (Join-Path $env:SystemRoot 'Temp') 5 Measure-VsFolder 'updateDownloads' (Join-Path $env:SystemRoot 'SoftwareDistribution\Download') 6 Measure-VsFolder 'cbsLogs' (Join-Path $env:SystemRoot 'Logs\CBS') 3 ) | Where-Object { $_ }) $data.systemFiles = @(Invoke-VsSafe 'Системные файлы на диске' { Get-ChildItem -LiteralPath ($env:SystemDrive + '\') -Force -File -ErrorAction Stop | Where-Object { $_.Name -in 'hiberfil.sys', 'pagefile.sys', 'swapfile.sys' } | ForEach-Object { [ordered]@{ name = $_.Name; sizeGB = Format-VsGB $_.Length } } }) $enabled = @($data.startupItems | Where-Object { $_['enabled'] }).Count $third = @($data.services | Where-Object { $_['thirdParty'] }).Count $summary = 'в автозагрузке: {0} (включено {1}), сторонних служб: {2}, своих заданий в планировщике: {3}' -f $data.startupItems.Count, $enabled, $third, $data.scheduledTasks.Count @{ Summary = $summary; Data = $data } } } @{ Id = 'security'; Title = 'Защита и безопасность'; Timeout = 60 Body = { $data = [ordered]@{} $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() $data.account = [ordered]@{ elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Состоит ли пользователь в группе «Администраторы» (даже если окно запущено без повышения прав). $data.account.memberOfAdministrators = Invoke-VsSafe 'Группы пользователя' { (Invoke-VsNative 'whoami.exe' '/groups /fo csv /nh' 15).Output -match 'S-1-5-32-544' } $data.localAccounts = Invoke-VsSafe 'Учётные записи' { $users = @(Get-LocalUser) $admins = $null try { $admins = @(Get-LocalGroupMember -SID 'S-1-5-32-544' -ErrorAction Stop).Count } catch { } [ordered]@{ total = $users.Count; enabled = @($users | Where-Object { $_.Enabled }).Count builtinAdminEnabled = (@($users | Where-Object { "$($_.SID)" -like '*-500' -and $_.Enabled }).Count -gt 0) guestEnabled = (@($users | Where-Object { "$($_.SID)" -like '*-501' -and $_.Enabled }).Count -gt 0) enabledWithoutPassword = @($users | Where-Object { $_.Enabled -and -not $_.PasswordRequired }).Count administrators = $admins } } $sys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' $data.uac = [ordered]@{ enabled = Get-VsReg $sys 'EnableLUA'; consentPromptAdmin = Get-VsReg $sys 'ConsentPromptBehaviorAdmin'; secureDesktop = Get-VsReg $sys 'PromptOnSecureDesktop' } $data.antivirus = @(Invoke-VsSafe 'Антивирусы (Центр безопасности Windows)' { Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName AntiVirusProduct | ForEach-Object { $state = [int]$_.productState $hex = '{0:X6}' -f $state [ordered]@{ name = $_.displayName; productState = $state; enabled = ($hex.Substring(2, 2) -in '10', '11'); upToDate = ($hex.Substring(4, 2) -eq '00') timestamp = $_.timestamp; executable = Protect-VsText $_.pathToSignedProductExe } } }) $data.defender = Invoke-VsSafe 'Microsoft Defender' { $m = Get-MpComputerStatus [ordered]@{ runningMode = $m.AMRunningMode; serviceEnabled = $m.AMServiceEnabled; antivirusEnabled = $m.AntivirusEnabled realTimeProtection = $m.RealTimeProtectionEnabled; tamperProtected = $m.IsTamperProtected # 65535 и больше — «нет данных» (например, Defender выключен сторонним антивирусом), а не возраст. signatureAgeDays = $(if ($m.AntivirusSignatureAge -ge 65535) { $null } else { $m.AntivirusSignatureAge }) signatureUpdated = $m.AntivirusSignatureLastUpdated; productVersion = $m.AMProductVersion quickScanAgeDays = $(if ($m.QuickScanAge -ge 65535) { $null } else { $m.QuickScanAge }); fullScanEnd = $m.FullScanEndTime } } $data.threats = @(Invoke-VsSafe 'Найденные угрозы' { Get-MpThreat | ForEach-Object { [ordered]@{ name = $_.ThreatName; severity = [int]$_.SeverityID; category = [int]$_.CategoryID; active = $_.IsActive; executed = $_.DidThreatExecute } } }) # Сторонний брандмауэр (например, Kaspersky) выключает брандмауэр Windows — это нормально. $data.firewallProducts = @(Invoke-VsSafe 'Брандмауэры (Центр безопасности Windows)' { Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName FirewallProduct | ForEach-Object { $hex = '{0:X6}' -f [int]$_.productState [ordered]@{ name = $_.displayName; productState = [int]$_.productState; enabled = ($hex.Substring(2, 2) -in '10', '11'); timestamp = $_.timestamp } } }) $data.firewall = @(Invoke-VsSafe 'Брандмауэр Windows' { Get-NetFirewallProfile | ForEach-Object { [ordered]@{ profile = "$($_.Name)"; enabled = "$($_.Enabled)" } } }) $data.smartScreen = [ordered]@{ explorer = Get-VsReg 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer' 'SmartScreenEnabled' policy = Get-VsReg 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' 'EnableSmartScreen' } $data.smb1 = [ordered]@{ server = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters' 'SMB1' clientStart = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Services\mrxsmb10' 'Start' } $data.remoteDesktop = [ordered]@{ denyConnections = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' 'fDenyTSConnections' nla = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' 'UserAuthentication' } $data.lsaProtection = Get-VsReg 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' 'RunAsPPL' $data.autorun = Get-VsReg 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' 'NoDriveTypeAutoRun' $data.powershellV2 = Get-VsReg 'HKLM:\SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' 'PowerShellVersion' $is = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' $data.proxy = [ordered]@{ enabled = Get-VsReg $is 'ProxyEnable'; server = Get-VsReg $is 'ProxyServer'; autoConfigUrl = Get-VsReg $is 'AutoConfigURL' } # hosts: активаторы и вредоносные программы любят блокировать здесь сайты обновлений и антивирусов. $data.hosts = Invoke-VsSafe 'Файл hosts' { $lines = @(Get-Content -LiteralPath (Join-Path $env:SystemRoot 'System32\drivers\etc\hosts') -ErrorAction Stop | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') }) [ordered]@{ entries = $lines.Count; lines = @($lines | Select-Object -First 100) } } # Корневые сертификаты: сторонние «корни» ставят антивирусы, рекламное ПО — и Минцифры. $data.rootCertificates = Invoke-VsSafe 'Корневые сертификаты' { $describe = { param($store) @(Get-ChildItem -LiteralPath $store -ErrorAction Stop | ForEach-Object { [ordered]@{ subject = ($_.Subject -replace '^.*?CN=([^,]+).*$', '$1'); notAfter = $_.NotAfter; thumbprint = $_.Thumbprint } }) } $machine = @(& $describe 'Cert:\LocalMachine\Root') $user = @(& $describe 'Cert:\CurrentUser\Root') [ordered]@{ machine = $machine; user = $user russianTrustedRoot = (@(@($machine) + @($user) | Where-Object { $_['subject'] -match 'Russian Trusted' }).Count -gt 0) } } $parts = @() # Антивирусы оставляют в Центре безопасности записи от старых версий — имена без повторов. $avOn = @($data.antivirus | Where-Object { $_['enabled'] } | ForEach-Object { $_['name'] } | Select-Object -Unique) if ($avOn.Count) { $parts += ('антивирус: {0}' -f ($avOn -join ', ')) } elseif ($data.antivirus.Count) { $parts += 'антивирус установлен, но выключен' } else { $parts += 'антивирус не найден' } $fwOn = @($data.firewallProducts | Where-Object { $_['enabled'] } | ForEach-Object { $_['name'] } | Select-Object -Unique) $fwOff = @($data.firewall | Where-Object { $_['enabled'] -eq 'False' }).Count if ($fwOn.Count) { $parts += ('брандмауэр: {0}' -f ($fwOn -join ', ')) } elseif ($data.firewall.Count) { $parts += $(if ($fwOff) { ('брандмауэр Windows выключен в профилях: {0}' -f $fwOff) } else { 'брандмауэр Windows включён' }) } if ($data.threats.Count) { $parts += ('угроз в журнале Defender: {0}' -f $data.threats.Count) } @{ Summary = ($parts -join ', '); Data = $data } } } ) # --------------------------------------------------------------------------- # Локальные выводы. Правило — это данные: id, важность, раздел, заголовок, «что сделать», # «что это даст» — и короткое условие Check над готовым отчётом. Тексты и id переедут # на сервер как есть, переписать придётся только условия. # # Check получает отчёт (@{ os; hardware; events; ... } — разобранные JSON блоков) и выдаёт # ничего (нет находки), строку (подробности) или словарь, переопределяющий поля находки: # @{ detail; severity; title; advice }. Внутри Check нет return: только вывод значений. $VsRulesVersion = '0.2.0' function ConvertTo-VsDate($value) { if ($null -eq $value -or "$value" -eq '') { return $null } if ($value -is [datetime]) { return $value } try { [datetime]::Parse("$value", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind) } catch { $null } } function Format-VsDate($value) { $d = ConvertTo-VsDate $value if ($d) { $d.ToLocalTime().ToString('dd.MM.yyyy') } else { '?' } } $VsBugchecks = @{ '0xA' = 'IRQL_NOT_LESS_OR_EQUAL'; '0x19' = 'BAD_POOL_HEADER'; '0x1A' = 'MEMORY_MANAGEMENT'; '0x1E' = 'KMODE_EXCEPTION_NOT_HANDLED' '0x3B' = 'SYSTEM_SERVICE_EXCEPTION'; '0x3D' = 'INTERRUPT_EXCEPTION_NOT_HANDLED'; '0x50' = 'PAGE_FAULT_IN_NONPAGED_AREA' '0x7E' = 'SYSTEM_THREAD_EXCEPTION_NOT_HANDLED'; '0x7F' = 'UNEXPECTED_KERNEL_MODE_TRAP'; '0x9F' = 'DRIVER_POWER_STATE_FAILURE' '0xA0' = 'INTERNAL_POWER_ERROR'; '0xC2' = 'BAD_POOL_CALLER'; '0xD1' = 'DRIVER_IRQL_NOT_LESS_OR_EQUAL'; '0xEF' = 'CRITICAL_PROCESS_DIED' '0x116' = 'VIDEO_TDR_FAILURE'; '0x124' = 'WHEA_UNCORRECTABLE_ERROR'; '0x133' = 'DPC_WATCHDOG_VIOLATION' '0x139' = 'KERNEL_SECURITY_CHECK_FAILURE'; '0x154' = 'UNEXPECTED_STORE_EXCEPTION' } # Сроки поддержки Windows по данным Microsoft на ноябрь 2026 года. На сервере таблица будет обновляться сама. $VsWindowsSupport = @( @{ Build = 19045; Name = 'Windows 10'; Consumer = '2025-10-14'; Business = '2025-10-14' } @{ Build = 22000; Name = 'Windows 11 21H2'; Consumer = '2023-10-10'; Business = '2024-10-08' } @{ Build = 22621; Name = 'Windows 11 22H2'; Consumer = '2024-10-08'; Business = '2025-10-14' } @{ Build = 22631; Name = 'Windows 11 23H2'; Consumer = '2025-11-11'; Business = '2026-11-10' } @{ Build = 26100; Name = 'Windows 11 24H2'; Consumer = '2026-10-13'; Business = '2027-10-12' } @{ Build = 26200; Name = 'Windows 11 25H2'; Consumer = '2027-10-12'; Business = '2028-10-10' } ) $VsRules = @( # --- Стабильность --- @{ Id = 'stability.bsod'; Severity = 'critical'; Area = 'Стабильность' Title = 'Были синие экраны' Advice = 'Если повторяются — обновите драйверы видеокарты и чипсета, уберите разгон и проверьте память (средство проверки памяти Windows). Точного виновника покажет разбор файла дампа.' Gain = 'Компьютер перестанет внезапно перезагружаться и терять несохранённую работу.' Check = { param($r) $list = @($r.events.critical90d | Where-Object { $_.id -eq 1001 }) if ($list.Count) { $codeText = 'неизвестен' if ("$($list[0].detail)" -match '^0x([0-9A-Fa-f]+)') { $codeText = '0x{0:X}' -f [Convert]::ToInt64($Matches[1], 16) if ($VsBugchecks.ContainsKey($codeText)) { $codeText += ' — ' + $VsBugchecks[$codeText] } } $text = 'За 90 дней: {0}. Последний — {1}, код {2}.' -f $list.Count, (Format-VsDate $list[0].time), $codeText if (@($r.events.critical90d | Where-Object { $_.provider -eq 'volmgr' -and $_.id -eq 161 }).Count) { $text += ' Файл дампа при этом не сохранился.' } $text } } } @{ Id = 'stability.unexpected-shutdown'; Severity = 'warning'; Area = 'Стабильность' Title = 'Компьютер внезапно выключался' Advice = 'Так выглядит пропадание питания, перегрев или зависание, после которого держали кнопку. Если свет не моргал и кнопку не держали — проверьте температуру и блок питания.' Gain = 'Меньше риск повредить файлы и диск при следующем таком выключении.' Check = { param($r) $list = @($r.events.critical90d | Where-Object { $_.provider -like '*Kernel-Power' -and $_.id -eq 41 -and ("$($_.detail)" -eq '0x0' -or -not $_.detail) }) if ($list.Count -ge 2) { 'За 90 дней: {0}, последнее — {1}. Синего экрана при этом не было.' -f $list.Count, (Format-VsDate $list[0].time) } } } @{ Id = 'stability.app-crashes'; Severity = 'info'; Area = 'Стабильность' Title = 'Программы часто падают' Advice = 'Обновите или переустановите эти программы; если падают сразу после запуска — проверьте, не мешает ли им антивирус.' Gain = 'Меньше потерянной работы и сообщений об ошибках.' Check = { param($r) $list = @($r.events.appCrashes30d | Where-Object { $_.hits -ge 3 }) if ($list.Count) { (($list | Select-Object -First 5 | ForEach-Object { '{0} — {1} раз за месяц' -f $_.app, $_.hits }) -join '; ') + '.' } } } # --- Железо и диски --- @{ Id = 'hardware.whea'; Severity = 'critical'; Area = 'Железо' Title = 'Аппаратные ошибки' Advice = 'Это сообщения самого процессора, памяти или шины о сбоях. Уберите разгон, проверьте температуру и память. Если ошибки повторяются — нужна диагностика у мастера.' Gain = 'Вовремя замеченная неисправность обходится дешевле, чем внезапный отказ.' Check = { param($r) $list = @($r.events.critical90d | Where-Object { $_.provider -like '*WHEA-Logger' }) if ($list.Count) { $errors = @($list | Where-Object { $_.level -le 2 }).Count $severity = if ($errors) { 'critical' } else { 'warning' } @{ severity = $severity; detail = ('За 90 дней: {0} (ошибок: {1}, исправленных сбоев: {2}), последняя — {3}.' -f $list.Count, $errors, ($list.Count - $errors), (Format-VsDate $list[0].time)) } } } } @{ Id = 'disk.errors'; Severity = 'critical'; Area = 'Диски' Title = 'Ошибки диска или файловой системы' Advice = 'Сделайте резервную копию важных файлов и проверьте диск (chkdsk). Повторяющиеся ошибки — повод заменить диск.' Gain = 'Сохранность данных: такие ошибки часто предшествуют отказу диска.' Check = { param($r) $list = @($r.events.critical90d | Where-Object { $_.provider -eq 'disk' -or $_.provider -like '*Ntfs' }) if ($list.Count) { $errors = @($list | Where-Object { $_.level -le 2 }).Count $severity = if ($errors) { 'critical' } else { 'warning' } @{ severity = $severity; detail = ('За 90 дней: {0} (из них ошибок: {1}), последнее — {2}.' -f $list.Count, $errors, (Format-VsDate $list[0].time)) } } } } @{ Id = 'disk.health'; Severity = 'critical'; Area = 'Диски' Title = 'Диск сообщает о проблемах' Advice = 'Срочно скопируйте важные данные на другой носитель и замените диск.' Gain = 'Данные останутся целы, даже если диск откажет.' Check = { param($r) $issues = @() foreach ($d in @($r.hardware.physicalDisks)) { if ($d.health -and $d.health -ne 'Healthy' -and $d.health -ne 'Unknown') { $issues += ('{0} — состояние {1}' -f $d.model, $d.health) } } foreach ($s in @($r.hardware.diskReliability)) { $model = (@($r.hardware.physicalDisks | Where-Object { "$($_.id)" -eq "$($s.id)" }) | Select-Object -First 1).model if ($s.readErrorsUncorrected -gt 0) { $issues += ('{0} — неисправимых ошибок чтения: {1}' -f $model, $s.readErrorsUncorrected) } if ($s.wearPercent -ge 80) { $issues += ('{0} — износ {1}%' -f $model, $s.wearPercent) } } if ($issues.Count) { ($issues -join '; ') + '.' } } } @{ Id = 'disk.system-space'; Severity = 'warning'; Area = 'Диски' Title = 'Мало места на системном диске' Advice = 'Очистите временные файлы (Параметры → Система → Память) и перенесите большие папки на другой диск. Если гибернация не нужна, её отключение освободит место под hiberfil.sys.' Gain = 'Windows и программам нужно свободное место для обновлений и временных файлов — без него всё тормозит, а обновления не ставятся.' Check = { param($r) $drive = if ($r.os.systemDrive) { $r.os.systemDrive } else { 'C:' } $vol = @($r.hardware.volumes | Where-Object { $_.drive -eq $drive }) | Select-Object -First 1 if ($vol -and $null -ne $vol.freeGB) { $severity = $null if ($vol.freeGB -lt 5 -or $vol.freePercent -lt 5) { $severity = 'critical' } elseif ($vol.freeGB -lt 20 -or $vol.freePercent -lt 10) { $severity = 'warning' } if ($severity) { $text = 'На {0} свободно {1:N1} ГБ из {2:N0} ГБ ({3}%).' -f $vol.drive, $vol.freeGB, $vol.sizeGB, $vol.freePercent $big = @($r.startup.systemFiles | Where-Object { $_.sizeGB -ge 4 } | ForEach-Object { '{0} — {1:N0} ГБ' -f $_.name, $_.sizeGB }) if ($big.Count) { $text += ' Из занятого: ' + ($big -join ', ') + '.' } @{ severity = $severity; detail = $text } } } } } @{ Id = 'disk.data-space'; Severity = 'info'; Area = 'Диски' Title = 'Почти заполнен диск с данными' Advice = 'Посмотрите, что занимает место (Параметры → Система → Память → Другие диски), и перенесите или удалите лишнее.' Gain = 'Заполненный почти до конца диск медленнее работает и быстрее фрагментируется.' Check = { param($r) $drive = if ($r.os.systemDrive) { $r.os.systemDrive } else { 'C:' } $full = @($r.hardware.volumes | Where-Object { $_.drive -ne $drive -and $null -ne $_.freePercent -and $_.freePercent -lt 10 -and $_.sizeGB -ge 32 }) if ($full.Count) { (($full | ForEach-Object { '{0} — свободно {1:N0} ГБ из {2:N0} ГБ ({3}%)' -f $_.drive, $_.freeGB, $_.sizeGB, $_.freePercent }) -join '; ') + '.' } } } @{ Id = 'memory.single-channel'; Severity = 'warning'; Area = 'Железо' Title = 'Память работает в одноканальном режиме' Advice = 'Добавьте вторую такую же планку (того же объёма и частоты) в парный слот — какой именно, написано в инструкции к материнской плате.' Gain = 'Вдвое больше пропускная способность памяти: заметно в играх, на встроенной графике и в тяжёлых программах.' Check = { param($r) $modules = @($r.hardware.memory.modules) if ($modules.Count -eq 1 -and $r.hardware.memory.slots -ge 2) { 'Установлена одна планка ({0:N0} ГБ, {1} МГц), а слотов — {2}.' -f $modules[0].sizeGB, $modules[0].configuredMHz, $r.hardware.memory.slots } } } @{ Id = 'memory.pressure'; Severity = 'warning'; Area = 'Быстродействие' Title = 'Не хватает оперативной памяти' Advice = 'Закройте лишние программы и вкладки браузера; если так постоянно — добавьте памяти.' Gain = 'Меньше подтормаживаний: Windows не придётся выгружать программы на диск.' Check = { param($r) $m = $r.perf.memory if ($m -and ($m.commitPercent -ge 85 -or $m.availableMB -lt 1024)) { 'Во время проверки было свободно {0:N1} ГБ, выделено {1}% от доступного.' -f ($m.availableMB / 1024), $m.commitPercent } } } @{ Id = 'devices.problems'; Severity = 'warning'; Area = 'Железо' Title = 'Устройства с ошибками' Advice = 'Установите драйверы с сайта производителя ноутбука, материнской платы или самого устройства.' Gain = 'Устройство заработает как положено.' Check = { param($r) $codes = @{ 1 = 'не настроено'; 10 = 'не удаётся запустить'; 18 = 'нужно переустановить драйвер'; 28 = 'драйвер не установлен'; 31 = 'драйвер не загружается'; 39 = 'драйвер повреждён или отсутствует'; 43 = 'устройство сообщило о сбое'; 52 = 'у драйвера нет подписи' } $list = @($r.hardware.problemDevices | Where-Object { $_.errorCode -ne 22 -and $_.errorCode -ne 45 }) if ($list.Count) { (($list | Select-Object -First 5 | ForEach-Object { $name = if ($_.name) { $_.name } else { $_.hardwareId } $reason = if ($codes.ContainsKey([int]$_.errorCode)) { $codes[[int]$_.errorCode] } else { 'код {0}' -f $_.errorCode } '{0} — {1}' -f $name, $reason }) -join '; ') + '.' } } } @{ Id = 'drivers.gpu-old'; Severity = 'info'; Area = 'Драйверы' Title = 'Старый драйвер видеокарты' Advice = 'Скачайте свежий драйвер с сайта NVIDIA, AMD или Intel — не через «Диспетчер устройств».' Gain = 'Исправления ошибок, стабильность и производительность в новых играх и программах.' Check = { param($r) $old = @($r.hardware.gpu | Where-Object { $_.name -match 'NVIDIA|GeForce|Radeon RX|Radeon Pro|\bArc\b' } | Where-Object { $d = ConvertTo-VsDate $_.driverDate $d -and ((Get-Date) - $d).TotalDays -gt 365 }) if ($old.Count) { (($old | ForEach-Object { '{0}: драйвер от {1}' -f $_.name, (Format-VsDate $_.driverDate) }) -join '; ') + '.' } } } # --- Windows и обновления --- @{ Id = 'windows.support'; Severity = 'critical'; Area = 'Windows' Title = 'Эта версия Windows больше не получает обновлений безопасности' Advice = 'Установите последнюю версию Windows 11 через Центр обновления (Параметры → Центр обновления Windows).' Gain = 'Исправления уязвимостей: без них компьютер беззащитен перед новыми атаками.' Check = { param($r) $build = [int]$r.os.build $edition = "$($r.os.editionId)" if ($build -and $edition -notmatch 'LTSC|EnterpriseS|Server') { $entry = if ($build -lt 22000) { $VsWindowsSupport[0] } else { $VsWindowsSupport | Where-Object { $_.Build -eq $build } | Select-Object -First 1 } if ($entry) { $endText = if ($edition -match 'Enterprise|Education') { $entry.Business } else { $entry.Consumer } $end = [datetime]::ParseExact($endText, 'yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture) $daysLeft = ($end - (Get-Date)).TotalDays $advice = $null if ($build -lt 22000) { $advice = 'Перейдите на Windows 11, если компьютер подходит, или подключите расширенные обновления безопасности (ESU).' } if ($daysLeft -lt 0) { @{ title = ('{0} больше не получает обновлений безопасности' -f $entry.Name); detail = ('Поддержка закончилась {0}.' -f $end.ToString('dd.MM.yyyy')); advice = $advice } } elseif ($daysLeft -lt 120) { @{ severity = 'warning'; title = ('Поддержка {0} скоро закончится' -f $entry.Name); detail = ('Обновления безопасности выходят до {0}.' -f $end.ToString('dd.MM.yyyy')); advice = $advice } } } } } } @{ Id = 'windows.activation'; Severity = 'warning'; Area = 'Windows' Title = 'Windows не активирована' Advice = 'Активируйте Windows лицензионным ключом (Параметры → Система → Активация).' Gain = 'Все функции и настройки персонализации без ограничений и напоминаний.' Check = { param($r) $win = @($r.activation.products | Where-Object { $_.application -eq 'windows' }) | Select-Object -First 1 if ($win) { if ([int]$win.licenseStatus -ne 1) { 'Состояние лицензии: {0}.' -f $(if ([int]$win.licenseStatus -eq 0) { 'не активирована' } else { 'пробный или ограниченный период' }) } elseif ("$($win.description)" -match 'KMSCLIENT' -and -not $r.os.computer.partOfDomain) { @{ title = 'Windows активирована через KMS вне организации' detail = 'Компьютер не в домене, а Windows активирована через KMS{0} — так обычно работают активаторы.' -f $(if ($win.kmsMachine) { ' (сервер {0})' -f $win.kmsMachine } else { '' }) advice = 'Активаторы часто приносят с собой вредоносные программы и отключают защиту. Надёжнее — лицензионный ключ.' } } } } } @{ Id = 'updates.stale'; Severity = 'warning'; Area = 'Обновления' Title = 'Обновления давно не устанавливались' Advice = 'Откройте Центр обновления Windows и нажмите «Проверить наличие обновлений». Если обновления не ставятся — это отдельная проблема, которую стоит разобрать.' Gain = 'Исправления безопасности и ошибок, которые Microsoft выпускает каждый месяц.' Check = { param($r) if ($r.updates) { $dates = @($r.updates.history | Where-Object { $_.result -eq 'succeeded' -and -not $_.isDefinition -and $_.operation -eq 1 } | ForEach-Object { ConvertTo-VsDate $_.date } | Where-Object { $_ } | Sort-Object -Descending) if (-not $dates.Count) { 'В истории Центра обновлений нет установленных обновлений Windows.' } elseif (((Get-Date) - $dates[0]).TotalDays -gt 45) { 'Последнее обновление установлено {0} — {1:N0} дн. назад.' -f $dates[0].ToLocalTime().ToString('dd.MM.yyyy'), ((Get-Date) - $dates[0]).TotalDays } } } } @{ Id = 'updates.disabled'; Severity = 'warning'; Area = 'Обновления' Title = 'Автоматические обновления отключены' Advice = 'Включите обновления обратно: многие «оптимизаторы» и сборки Windows отключают их, оставляя систему без исправлений безопасности.' Gain = 'Windows снова будет получать исправления сама.' Check = { param($r) $reasons = @() if ("$($r.updates.policies.noAutoUpdate)" -eq '1') { $reasons += 'политикой NoAutoUpdate' } $wu = @($r.updates.services | Where-Object { $_.name -eq 'wuauserv' }) | Select-Object -First 1 if ($wu -and "$($wu.start)" -eq '4') { $reasons += 'служба «Центр обновления Windows» отключена' } $pause = ConvertTo-VsDate $r.updates.policies.pauseUpdatesExpiry if ($pause -and $pause -gt (Get-Date)) { $reasons += ('обновления приостановлены до {0}' -f $pause.ToLocalTime().ToString('dd.MM.yyyy')) } if ($reasons.Count) { 'Причина: ' + ($reasons -join '; ') + '.' } } } @{ Id = 'updates.pending-reboot'; Severity = 'info'; Area = 'Обновления' Title = 'Обновления ждут перезагрузки' Advice = 'Перезагрузите компьютер — именно «Перезагрузка», а не «Завершение работы».' Gain = 'Установленные исправления начнут действовать.' Check = { param($r) if ($r.updates.pendingReboot.componentServicing -or $r.updates.pendingReboot.windowsUpdate) { 'Часть обновлений установлена, но вступит в силу только после перезагрузки.' } } } # --- Безопасность --- @{ Id = 'security.antivirus'; Severity = 'critical'; Area = 'Безопасность' Title = 'Антивирус не работает' Advice = 'Включите встроенный Microsoft Defender или установленный антивирус и убедитесь, что его базы обновляются.' Gain = 'Защита от вирусов, шифровальщиков и кражи паролей.' Check = { param($r) if ($r.security) { $av = @($r.security.antivirus) $on = @($av | Where-Object { $_.enabled }) $defenderOn = [bool]$r.security.defender.antivirusEnabled -and [bool]$r.security.defender.realTimeProtection if (-not $on.Count -and -not $defenderOn) { $names = @($av | ForEach-Object { $_.name } | Select-Object -Unique) if ($names.Count) { 'Антивирус установлен ({0}), но выключен.' -f ($names -join ', ') } else { 'Работающий антивирус не найден.' } } else { # Антивирусы оставляют записи от старых версий — «устарело» только если ни одна запись этого имени не свежая. $stale = @($on | Group-Object { $_.name } | Where-Object { -not @($_.Group | Where-Object { $_.upToDate }).Count } | ForEach-Object { $_.Name }) if ($stale.Count) { @{ severity = 'warning'; title = 'Базы антивируса устарели'; detail = ('{0} сообщает, что его базы не обновлены.' -f ($stale -join ', ')); advice = 'Откройте антивирус и запустите обновление баз; если не обновляется — проверьте лицензию и подключение к интернету.' } } } } } } @{ Id = 'security.threats'; Severity = 'critical'; Area = 'Безопасность' Title = 'Найдены активные угрозы' Advice = 'Откройте «Безопасность Windows» → «Защита от вирусов и угроз» и удалите угрозы; затем выполните полную проверку.' Gain = 'Вредоносная программа перестанет работать на компьютере.' Check = { param($r) $active = @($r.security.threats | Where-Object { $_.active }) if ($active.Count) { 'Defender отмечает: ' + (@($active | ForEach-Object { $_.name } | Select-Object -Unique) -join ', ') + '.' } } } @{ Id = 'security.firewall'; Severity = 'warning'; Area = 'Безопасность' Title = 'Брандмауэр выключен' Advice = 'Включите брандмауэр: «Безопасность Windows» → «Брандмауэр и защита сети».' Gain = 'Защита от атак из сети, особенно в общественном Wi-Fi.' Check = { param($r) if ($r.security) { $products = @($r.security.firewallProducts | Where-Object { $_.enabled }) $names = @{ Private = 'частных'; Public = 'общедоступных' } $off = @($r.security.firewall | Where-Object { $_.enabled -eq 'False' -and $names.ContainsKey("$($_.profile)") }) if (-not $products.Count -and $off.Count) { 'Брандмауэр Windows выключен для {0} сетей, другого брандмауэра нет.' -f (($off | ForEach-Object { $names["$($_.profile)"] }) -join ' и ') } } } } @{ Id = 'security.secure-boot'; Severity = 'warning'; Area = 'Безопасность' Title = 'Безопасная загрузка (Secure Boot) выключена' Advice = 'Включите Secure Boot в настройках UEFI (BIOS), обычно в разделе Boot или Security.' Gain = 'Защита от вирусов, которые запускаются раньше Windows; некоторые игры с античитом без неё не запускаются.' Check = { param($r) if ($r.os.firmware.type -eq 'UEFI' -and "$($r.os.firmware.secureBoot)" -eq '0') { 'Компьютер загружается в режиме UEFI, но Secure Boot выключен.' } } } @{ Id = 'security.uac'; Severity = 'warning'; Area = 'Безопасность' Title = 'Контроль учётных записей (UAC) отключён' Advice = 'Включите UAC: Панель управления → Учётные записи пользователей → Изменить параметры контроля учётных записей.' Gain = 'Программы не смогут незаметно получить права администратора.' Check = { param($r) if ("$($r.security.uac.enabled)" -eq '0') { 'Любая программа получает права администратора без запроса.' } } } @{ Id = 'security.smb1'; Severity = 'warning'; Area = 'Безопасность' Title = 'Включён устаревший протокол SMBv1' Advice = 'Отключите «Поддержку общего доступа к файлам SMB 1.0/CIFS» в компонентах Windows.' Gain = 'Закрыта дыра, через которую распространялись WannaCry и NotPetya.' Check = { param($r) $smb = $r.security.smb1 if ($smb -and ("$($smb.server)" -eq '1' -or ($null -ne $smb.clientStart -and "$($smb.clientStart)" -ne '4'))) { 'Компьютер принимает или использует подключения по SMBv1.' } } } @{ Id = 'security.hosts'; Severity = 'warning'; Area = 'Безопасность' Title = 'В файле hosts заблокированы обновления или антивирусы' Advice = 'Так делают активаторы и вредоносные программы. Проверьте компьютер антивирусом и уберите эти строки из файла hosts.' Gain = 'Windows и антивирус снова смогут обновляться.' Check = { param($r) $lines = @($r.security.hosts.lines | Where-Object { $_ -match 'microsoft|windowsupdate|msftncsi|kaspersky|drweb|eset|avast|avg\.com|nod32|bitdefender|norton|malwarebytes|virustotal' }) if ($lines.Count) { $hosts = @($lines | ForEach-Object { @($_ -split '\s+')[1] } | Where-Object { $_ } | Select-Object -Unique) 'Заблокировано адресов: {0}, например: {1}.' -f $lines.Count, (($hosts | Select-Object -First 4) -join ', ') } } } # --- Быстродействие и порядок --- @{ Id = 'perf.startup'; Severity = 'info'; Area = 'Быстродействие' Title = 'Много программ в автозагрузке' Advice = 'Отключите ненужные: Диспетчер задач → Автозагрузка приложений. Сами программы останутся, просто не будут стартовать вместе с Windows.' Gain = 'Быстрее включение и больше свободной памяти сразу после входа.' Check = { param($r) # Служебные имена вроде com.todesktop.25020447d4kq915 заменяем названием из описания файла. $names = @($r.startup.startupItems | Where-Object { $_.enabled } | ForEach-Object { $name = "$($_.name)" if ($_.product -and ($name -match '^(com|org|net|io|app)\.' -or $name -match '[0-9a-f]{12,}' -or $name -match '^\{')) { "$($_.product)" } else { $name } } | Select-Object -Unique) if ($names.Count -ge 8) { 'Включено {0}: {1}{2}.' -f $names.Count, (($names | Select-Object -First 6) -join ', '), $(if ($names.Count -gt 6) { ' и другие' } else { '' }) } } } @{ Id = 'perf.pagefile-off'; Severity = 'warning'; Area = 'Быстродействие' Title = 'Файл подкачки отключён' Advice = 'Верните автоматический размер файла подкачки: Система → О программе → Дополнительные параметры системы → Быстродействие → Дополнительно.' Gain = 'Программы не будут падать при нехватке памяти, а при синем экране сохранится дамп для разбора.' Check = { param($r) $pf = $r.startup.pagefile if ($pf -and -not $pf.automatic -and -not @($pf.settings).Count -and -not @($pf.usage).Count) { 'Ни на одном диске нет файла подкачки.' } } } @{ Id = 'perf.power-saver'; Severity = 'info'; Area = 'Быстродействие' Title = 'Включена экономия энергии' Advice = 'Выберите схему «Сбалансированная»: Панель управления → Электропитание.' Gain = 'Процессор перестанет искусственно сбрасывать частоту.' Check = { param($r) if ($r.startup.powerPlan.kind -eq 'powerSaver' -and [int]$r.os.computer.pcSystemType -eq 1) { 'На настольном компьютере выбрана схема «Экономия энергии».' } } } @{ Id = 'cleanup.temp'; Severity = 'info'; Area = 'Порядок' Title = 'Можно освободить место' Advice = 'Удалите временные файлы: Параметры → Система → Память → Временные файлы.' Gain = 'Свободное место на системном диске.' Check = { param($r) $mb = (@($r.startup.folders | Where-Object { $_.kind -eq 'userTemp' -or $_.kind -eq 'windowsTemp' -or $_.kind -eq 'updateDownloads' } | ForEach-Object { [double]$_.sizeMB }) | Measure-Object -Sum).Sum if ($mb -ge 2048) { 'Временные файлы и скачанные обновления занимают {0:N1} ГБ.' -f ($mb / 1024) } } } ) # Одно правило → находка или $null. function Invoke-VsRule($rule, $report) { $out = @(& $rule.Check $report | Where-Object { $null -ne $_ -and "$_" -ne '' }) | Select-Object -Last 1 if ($null -eq $out) { return $null } $finding = [ordered]@{ id = $rule.Id; severity = $rule.Severity; area = $rule.Area; title = $rule.Title; detail = $null; advice = $rule.Advice; gain = $rule.Gain } if ($out -is [hashtable]) { $out.Keys | ForEach-Object { if ($null -ne $out[$_]) { $finding[$_] = $out[$_] } } } else { $finding['detail'] = "$out" } $finding } # Запуск правил: каждое под своей защитой — сбой одного не мешает остальным. # ВАЖНО (Windows PowerShell 5.1): «Типы аргумента не совпадают» здесь давал индекс внутри индекса # над упорядоченным словарём — $таблица[$_['ключ']] в Sort-Object. Такие выражения не использовать: # значение сначала в строку ("$($_['ключ'])") или в переменную. Перебор — через ForEach-Object. function Invoke-VsRules($report) { $findings = New-Object 'System.Collections.Generic.List[object]' $failed = New-Object 'System.Collections.Generic.List[object]' $VsRules | ForEach-Object { $rule = $_ try { $finding = Invoke-VsRule $rule $report if ($null -ne $finding) { $findings.Add($finding) } } catch { $where = (@("$($_.ScriptStackTrace)" -split "`n") | Select-Object -First 1) $failed.Add([ordered]@{ id = $rule.Id; error = ('{0} [{1}]' -f $_.Exception.Message, "$where".Trim()) }) } } # Порядок по важности — раскладкой по корзинам, без сортировки по вычисляемому ключу. $sorted = New-Object 'System.Collections.Generic.List[object]' 'critical', 'warning', 'info' | ForEach-Object { $level = $_ $findings | Where-Object { "$($_['severity'])" -eq $level } | ForEach-Object { $sorted.Add($_) } } $findings | Where-Object { @('critical', 'warning', 'info') -notcontains "$($_['severity'])" } | ForEach-Object { $sorted.Add($_) } [pscustomobject]@{ Findings = $sorted.ToArray() Failed = $failed.ToArray() } } # --------------------------------------------------------------------------- # Окно $xaml = @'