EmoCheckを自動アップデートするツール

この記事は公開から2年以上経過しています。

Emotet検知ツールを毎日実行する運用ルールのある会社も少なくないと思いますが、EmoCheckにはバージョンチェックや自己アップデート機能がないため、定期的に自分で最新リリースをチェックしてアップデートする必要があるなど少々面倒です。

また、私がEmoCheckを定期実行しているPCはHDDのためなのか、EmoCheckスキャン中はI/O負荷が高すぎてPCの調子が悪くなってしまうことから、EmoCheckの自動更新およびEmoCheckをアイドル優先度のプロセスとして実行できる簡易ツールを作成してみました。

本ツールは2022.5.26時点のものです。
EmoCheckの仕様やGitHubのリポジトリ構成などが変わると正しく機能しなくなる可能性がありますので、予めご了承ください(As Is)。

エラーが発生した場合はログを記録して処理を終了します(EmoCheckは実行しない)。


サンプルソースコード(C#)

ビルド環境は Visual Studio 2022(or 2019) .NET Framework 4.7.2です。
(当初.NET 6で作成しましたがバイナリファイルサイズが大きすぎたので急遽変更。)

ソースとバイナリ一式をこちらに用意しましたので、宜しければお使い下さい。VirusTotalにてウイルスチェック済です。

// EXCEEDSYSTEM EmoCheckUpdater
// https://www.exceedsystem.net/2022/05/26/how-to-update-emocheck-automatically
// License: MIT
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;

namespace EmoCheckUpdater
{
    internal static class Program
    {
        private const string APP_NAME = "EmoCheckUpdater";
        private const string UPDATE_INFO_FILENAME = "updateinfo.json";
        private const string EMOCHECK_FILENAME = "emocheck.exe";
        private const string LOG_FILENAME = "emocheckupdater.log";
        private const string EMOCHECK_API_URL = "https://api.github.com/repos/JPCERTCC/EmoCheck/releases/latest";
        private const string USERAGENT_NAME = "Mozilla/5.0 (Windows NT 10.0)";
        private const ProcessPriorityClass EMOCHECK_PROCESS_PRIORITY = ProcessPriorityClass.Idle;

        private static readonly string MUTEX_NAME = $@"Global\MTX{Assembly.GetEntryAssembly().GetCustomAttribute<GuidAttribute>().Value}";

        [STAThread]
        public static int Main(string[] args)
        {
            if (Mutex.TryOpenExisting(MUTEX_NAME, out var mutex))
            {
                Log($"{APP_NAME} is already running.");
                mutex.Dispose();
                return -1;
            }

            using (mutex = new Mutex(true, MUTEX_NAME))
            using (new Cleanup(() => mutex.ReleaseMutex()))
            {
                string ARCH = String.Empty;
                switch (Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"))
                {
                    case "x86":
                        ARCH = "x86";
                        break;

                    case "AMD64":
                        ARCH = "x64";
                        break;
                }
                if (ARCH.Length == 0)
                {
                    Log("Unsupported processor architecture.");
                    return -1;
                }

                try
                {
                    Log($"{APP_NAME} has started.");

                    var updateInfo = new UpdateInfo();
                    if (!File.Exists(UPDATE_INFO_FILENAME))
                    {
                        File.WriteAllText(UPDATE_INFO_FILENAME, JsonSerializer.Serialize<UpdateInfo>(updateInfo));
                    }
                    updateInfo = JsonSerializer.Deserialize<UpdateInfo>(File.ReadAllText(UPDATE_INFO_FILENAME));
                    if (updateInfo == null)
                    {
                        Log($"'{UPDATE_INFO_FILENAME}' is invalid data structure.");
                        return -1;
                    }

                    Latest latest = null;
                    using (var httpClient = new HttpClient())
                    {
                        httpClient.DefaultRequestHeaders.Add("User-Agent", USERAGENT_NAME);

                        latest = httpClient.GetFromJsonAsync<Latest>(EMOCHECK_API_URL).Result;
                        if (latest == null)
                        {
                            Log("Failed to get EmoCheck update information.");
                            return -1;
                        }

                        if (latest.tag_name != updateInfo.CurrentTagName)
                        {
                            if (latest.assets == null || latest.assets.Length == 0)
                            {
                                Log("Failed to get the tag information of EmoCheck.");
                                return -1;
                            }

                            var downloadUrl = latest.assets.First(asset => asset.browser_download_url.EndsWith($"{ARCH}.exe"))?.browser_download_url;
                            if (string.IsNullOrEmpty(downloadUrl))
                            {
                                Log($"Failed to get EmoCheck download URL.");
                                return -1;
                            }

                            var emocheckBin = httpClient.GetByteArrayAsync(downloadUrl).Result;
                            if (emocheckBin == null)
                            {
                                Log($"Failed to download the latest version of EmoCheck.");
                                return -1;
                            }

                            using (var fs = new FileStream(EMOCHECK_FILENAME, FileMode.Create, FileAccess.Write))
                                fs.Write(emocheckBin, 0, emocheckBin.Length);

                            Log($"Downloaded the latest version of EmoCheck. ({downloadUrl})");

                            updateInfo.CurrentTagName = latest.tag_name;

                            var js = JsonSerializer.Serialize<UpdateInfo>(updateInfo);
                            File.WriteAllText(UPDATE_INFO_FILENAME, js);
                        }
                    }

                    var emoCheckProcessStartupInfo = new ProcessStartInfo
                    {
                        UseShellExecute = false,
                        FileName = Path.Combine(Environment.CurrentDirectory, EMOCHECK_FILENAME),
                        Arguments = string.Join(" ", args),
                        CreateNoWindow = args.Select(s => s.ToUpper()).Contains("/QUIET"),
                    };

                    using (var emoCheckProcess = Process.Start(emoCheckProcessStartupInfo))
                    {
                        if (emoCheckProcess == null)
                        {
                            Log("Failed to start EmoCheck.");
                            return -1;
                        }

                        Log($"EmoCheck has started. ({latest.tag_name})");

                        emoCheckProcess.PriorityClass = EMOCHECK_PROCESS_PRIORITY;
                        emoCheckProcess.WaitForExit();

                        Log($"EmoCheck has completed.");
                    }
                }
                catch (Exception ex)
                {
                    Log($"An unexpected error has occurred. ({ex.Message.Replace("\r\n", "␍␊")})");
                    return -1;
                }

                Log($"{APP_NAME} has completed.");

                return 0;
            }
        }

        private static void Log(string msg)
        {
            try
            {
                using (var proc = Process.GetCurrentProcess())
                    File.AppendAllText(LOG_FILENAME, $"{DateTime.Now:yyyy.MM.dd HH:mm:ss.fff} ({proc.Id}) {msg}\n");
            }
            catch { }
        }

        internal sealed class Cleanup : IDisposable
        {
            private readonly Action _cleanupAction;

            public Cleanup(Action cleanUpAction) => _cleanupAction = cleanUpAction;

            public void Dispose() => _cleanupAction();
        }

#pragma warning disable IDE1006

        internal sealed class Latest
        {
            public string tag_name { get; set; } = string.Empty;

            public Asset[] assets { get; set; } = Array.Empty<Asset>();

            public sealed class Asset
            {
                public string updated_at { get; set; } = string.Empty;

                public string browser_download_url { get; set; } = string.Empty;
            }
        }

#pragma warning restore IDE1006

        internal sealed class UpdateInfo
        {
            public string CurrentTagName { get; set; } = string.Empty;
        }
    }
}

GitHub Gist


使い方

Binフォルダ内のEmoCheckUpdater.exeを実行するだけです。
(私はタスクスケジューラーに登録して1日1回実行されるようにしています。)

EmoCheckに引数を渡したい場合はEmoCheckUpdaterへ引数として渡してください。
渡された引数は全てEmoCheckへパススルーされます。
例えばEmoCheckのコンソールウィンドウを表示せずバックグラウンドで実行したい場合にはEmoCheckUpdater.exe /quietのようなパラメータで実行します。

ツールの処理結果(EmoCheckの実行結果ではありません)は、カレントディレクトリ内のログファイルemocheckupdater.logに記録しています。


動作について

  1. 初回起動時にGitHubから最新リリースバージョンのEmoCheck実行ファイルをダウンロードし、カレントディレクトリ内にファイル名emocheck.exeとして保存します。

  2. ダウンロードしたリリースバージョン名を、カレントディレクトリ内の設定ファイルupdateinfo.jsonに保存します。

  3. カレントディレクトリ内のemocheck.exeをアイドルプロセスとして起動します。

  4. 起動2回目以降はupdateinfo.jsonに保存されているリリースバージョンとGitHubの最新リリースバージョンを比較し、異なる場合はGitHubから最新リリースバージョンのEmoCheck実行ファイルをダウンロードしてカレントディレクトリのemocheck.exeを上書き(バージョンアップ)します。

  5. カレントディレクトリのemocheck.exeを優先度低(アイドリングプロセス)で実行します。


参考ウェブサイトなど

以上です。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする