From 1ec13aab436a491dc963605f50373a504b1ac916 Mon Sep 17 00:00:00 2001 From: Takashi Hashida Date: Mon, 24 Aug 2026 22:36:19 +0900 Subject: [PATCH] Add integrate test procedure --- .../Configuration/ConfigLoaderTests.cs | 784 ++++++----- docs/testconfig/01_all-disabled.json | 64 + docs/testconfig/02_strict.json | 75 + .../MiXedCase_Sample.TxT" | 1 + ...\211\210 (\346\234\200\347\265\202)#1.TxT" | 1 + ...6\343\202\260\343\202\262\343\202\264.TxT" | 1 + ...\262\273_100%\351\201\224\346\210\220.CsV" | 3 + ...10\346\211\213\351\240\206\346\233\270.md" | 1240 +++++++++++++++++ 8 files changed, 1789 insertions(+), 380 deletions(-) create mode 100644 docs/testconfig/01_all-disabled.json create mode 100644 docs/testconfig/02_strict.json create mode 100644 "docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/MiXedCase_Sample.TxT" create mode 100644 "docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\206\343\202\271\343\203\210\350\263\207\346\226\231+\346\224\271\350\250\202-\347\254\2542\347\211\210 (\346\234\200\347\265\202)#1.TxT" create mode 100644 "docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\221\343\203\224\343\203\227\343\203\232\343\203\235_\343\202\254\343\202\256\343\202\260\343\202\262\343\202\264.TxT" create mode 100644 "docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\345\243\262\344\270\212&\347\265\214\350\262\273_100%\351\201\224\346\210\220.CsV" create mode 100644 "docs/\351\200\243\347\265\220\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206\346\233\270.md" diff --git a/BrowserGuard.Tests/Configuration/ConfigLoaderTests.cs b/BrowserGuard.Tests/Configuration/ConfigLoaderTests.cs index b986da3..7c8b65b 100644 --- a/BrowserGuard.Tests/Configuration/ConfigLoaderTests.cs +++ b/BrowserGuard.Tests/Configuration/ConfigLoaderTests.cs @@ -1,380 +1,404 @@ -using Xunit; -using BrowserGuard.Configuration; - -namespace BrowserGuard.Tests.Configuration -{ - public class ConfigLoaderTests - { - [Fact] - public void ParsesNestedFeatureGroups() - { - var json = """ - { - "NetLogger": { - "Enabled": false, - "Sender": { "Endpoint": "https://example.com/api/log" }, - "UrlAccess": true, - "Browsing": true, - "Upload": true, - "Download": false, - "Print": true - }, - "UploadGuard": { - "Enabled": false, - "BlockedExtensions": [".exe", ".zip"] - }, - "SettingPageFilter": { - "Enabled": true, - "BlockedPrefixes": ["edge://settings/", "edge://flags/"] - } - } - """; - - var config = ConfigLoader.ParseConf(json); - - Assert.Equal("https://example.com/api/log", config.NetLogger.Sender.Endpoint); - Assert.True(config.NetLogger.UrlAccess); - Assert.False(config.NetLogger.Download); - - Assert.False(config.UploadGuard.Enabled); - Assert.Equal(new[] { ".exe", ".zip" }, config.UploadGuard.BlockedExtensions); - - Assert.True(config.SettingPageFilter.Enabled); - Assert.Equal(new[] { "edge://settings/", "edge://flags/" }, config.SettingPageFilter.BlockedPrefixes); - } - - [Fact] - public void ParsesUploadPathsAndExtensions() - { - var json = """ - { - "UploadGuard": { - "Enabled": true, - "BlockedExtensions": [".exe"], - "AllowedExtensions": [".pdf", ".docx"], - "AllowedPaths": ["^C:\\\\Users\\\\[^\\\\]+\\\\Documents\\\\", "^D:\\\\Share\\\\"], - "BlockedPaths": ["\\\\Confidential\\\\"] - } - } - """; - - var config = ConfigLoader.ParseConf(json); - - Assert.True(config.UploadGuard.Enabled); - Assert.Equal(new[] { ".exe" }, config.UploadGuard.BlockedExtensions); - Assert.Equal(new[] { ".pdf", ".docx" }, config.UploadGuard.AllowedExtensions); - Assert.Equal( - new[] { @"^C:\\Users\\[^\\]+\\Documents\\", @"^D:\\Share\\" }, - config.UploadGuard.AllowedPaths); - Assert.Equal(new[] { @"\\Confidential\\" }, config.UploadGuard.BlockedPaths); - } - - [Fact] - public void UploadPathListsDefaultToEmpty() - { - var config = ConfigLoader.ParseConf("""{ "UploadGuard": { "Enabled": true } }"""); - - // Empty allowed lists mean "no restriction from that rule". - Assert.Empty(config.UploadGuard.AllowedExtensions); - Assert.Empty(config.UploadGuard.AllowedPaths); - Assert.Empty(config.UploadGuard.BlockedPaths); - Assert.Equal(new[] { ".exe", ".bat", ".cmd", ".js", ".vbs" }, config.UploadGuard.BlockedExtensions); - } - - [Fact] - public void ParsesStartupPrograms() - { - var json = """ - { - "StartupLauncher": { - "Enabled": true, - "Programs": [ - { - "Path": "C:\\Program Files\\Contoso\\agent.exe", - "Arguments": ["--mode", "kiosk"], - "WorkingDirectory": "C:\\Program Files\\Contoso", - "EnvironmentVariables": { "CONTOSO_PROFILE": "default", "LANG": "ja" }, - "Sha256": "abc123" - }, - { - "Path": "C:\\Tools\\notify.exe" - } - ] - } - } - """; - - var config = ConfigLoader.ParseConf(json); - - Assert.True(config.StartupLauncher.Enabled); - Assert.Equal(2, config.StartupLauncher.Programs.Length); - - var first = config.StartupLauncher.Programs[0]; - Assert.Equal(@"C:\Program Files\Contoso\agent.exe", first.Path); - Assert.Equal(new[] { "--mode", "kiosk" }, first.Arguments); - Assert.Equal(@"C:\Program Files\Contoso", first.WorkingDirectory); - Assert.Equal("default", first.EnvironmentVariables["CONTOSO_PROFILE"]); - Assert.Equal("ja", first.EnvironmentVariables["LANG"]); - Assert.Equal("abc123", first.Sha256); - - // Everything but the path may be left out. - var second = config.StartupLauncher.Programs[1]; - Assert.Equal(@"C:\Tools\notify.exe", second.Path); - Assert.Empty(second.Arguments); - Assert.Equal("", second.WorkingDirectory); - Assert.Empty(second.EnvironmentVariables); - Assert.Equal("", second.Sha256); - } - - [Fact] - public void ReadsWhereTheLogIsKept() - { - var json = """ - { - "NetLogger": { - "Enabled": true, - "Sender": { - "Enabled": true, - "Endpoint": "https://collector.example.com/log", - "OnSendFailure": { - "SaveLocally": true, - "RetryIntervalMinutes": 15, - "MaxSizeMB": 50 - } - }, - "LocalFile": { - "Enabled": true, - "Directory": "D:\\logs", - "MaxDays": 90, - "MaxSizeMB": 250 - } - } - } - """; - - var config = ConfigLoader.ParseConf(json).NetLogger; - - Assert.True(config.Enabled); - Assert.True(config.LocalFile.Enabled); - Assert.Equal(@"D:\logs", config.LocalFile.Directory); - Assert.Equal(90, config.LocalFile.MaxDays); - Assert.Equal(250, config.LocalFile.MaxSizeMB); - Assert.True(config.Sender.Enabled); - Assert.True(config.Sender.OnSendFailure.SaveLocally); - Assert.Equal(15, config.Sender.OnSendFailure.RetryIntervalMinutes); - Assert.Equal(50, config.Sender.OnSendFailure.MaxSizeMB); - } - - // 0 asks for no limit rather than for a limit of nothing. - [Fact] - public void ReadsAnUnlimitedSpool() - { - var config = ConfigLoader.ParseConf( - """{"NetLogger":{"Sender":{"OnSendFailure":{"SaveLocally":true,"MaxSizeMB":0}}}}"""); - - Assert.Equal(0, config.NetLogger.Sender.OnSendFailure.MaxSizeMB); - } - - // Nothing may be written to this machine unless it was asked for. - [Fact] - public void KeepsNothingLocallyByDefault() - { - var config = ConfigLoader.ParseConf("{}").NetLogger; - - Assert.False(config.Enabled); - Assert.False(config.LocalFile.Enabled); - Assert.False(config.Sender.Enabled); - Assert.False(config.Sender.OnSendFailure.SaveLocally); - Assert.Equal("", config.LocalFile.Directory); - } - - // The files that ship with the installer have to stay loadable. - [Theory] - [InlineData("Resources/BrowserGuard.json")] - [InlineData("BrowserGuard/BrowserGuard.sample.json")] - public void ParsesTheShippedConfigFiles(string relativePath) - { - var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); - var path = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); - Assert.True(File.Exists(path), $"not found: {path}"); - - var config = ConfigLoader.ParseConf(File.ReadAllText(path)); - - // A member that failed to bind would come back as the default, so a - // group is spot checked rather than only asserting no exception. - Assert.NotNull(config.StartupLauncher.Programs); - Assert.NotEmpty(config.SettingPageFilter.BlockedPrefixes); - Assert.NotEqual(0, config.NetLogger.LocalFile.MaxDays); - Assert.NotEqual(0, config.NetLogger.Sender.OnSendFailure.MaxSizeMB); - Assert.NotNull(config.UsageTimeLimit.AllowedTimeRanges); - Assert.NotEqual("", config.UsageTimeLimit.OnExceeded.Action); - } - - [Fact] - public void ReadsTheUsageTimeLimit() - { - var json = """ - { - "UsageTimeLimit": { - "Enabled": true, - "MaxContinuousMinutes": 240, - "AllowedTimeRanges": [ - { "Start": "09:00", "End": "12:00" }, - { "Start": "13:00", "End": "18:00" } - ], - "OnExceeded": { - "Action": "Terminate", - "GraceSeconds": 90, - "ReWarnIntervalMinutes": 5 - } - } - } - """; - - var config = ConfigLoader.ParseConf(json); - - Assert.True(config.UsageTimeLimit.Enabled); - Assert.Equal(240, config.UsageTimeLimit.MaxContinuousMinutes); - Assert.Equal(2, config.UsageTimeLimit.AllowedTimeRanges.Length); - Assert.Equal("09:00", config.UsageTimeLimit.AllowedTimeRanges[0].Start); - Assert.Equal("18:00", config.UsageTimeLimit.AllowedTimeRanges[1].End); - Assert.Equal("Terminate", config.UsageTimeLimit.OnExceeded.Action); - Assert.Equal(90, config.UsageTimeLimit.OnExceeded.GraceSeconds); - Assert.Equal(5, config.UsageTimeLimit.OnExceeded.ReWarnIntervalMinutes); - } - - // Nothing may start closing the browser unless it was asked for. - [Fact] - public void UsageTimeLimitDefaultsToDisabledAndWarningOnly() - { - var config = ConfigLoader.ParseConf("{}"); - - Assert.False(config.UsageTimeLimit.Enabled); - Assert.Equal(0, config.UsageTimeLimit.MaxContinuousMinutes); - Assert.Empty(config.UsageTimeLimit.AllowedTimeRanges); - Assert.Equal("WarnOnly", config.UsageTimeLimit.OnExceeded.Action); - } - - [Fact] - public void ReadsTheTabCountLimit() - { - var json = """ - { - "TabCountLimit": { - "Enabled": true, - "MaxCount": 20 - } - } - """; - - var config = ConfigLoader.ParseConf(json); - - Assert.True(config.TabCountLimit.Enabled); - Assert.Equal(20, config.TabCountLimit.MaxCount); - } - - // Zero is "no limit", so a config without a number cannot start warning - // about every tab. - [Fact] - public void TabCountLimitDefaultsToDisabledWithoutALimit() - { - var config = ConfigLoader.ParseConf("{}"); - - Assert.False(config.TabCountLimit.Enabled); - Assert.Equal(0, config.TabCountLimit.MaxCount); - } - - [Fact] - public void ReadsTheUploadFileBridge() - { - var json = """ - { - "UploadFileBridge": { - "Enabled": true, - "Destination": "\\\\fileserver\\audit\\%PCNAME%", - "MaxSizeMB": 100, - "BlockedExtensions": [".tmp"], - "AllowedExtensions": [".xlsx", ".docx"] - } - } - """; - - var config = ConfigLoader.ParseConf(json); - - Assert.True(config.UploadFileBridge.Enabled); - // The macros are left as they stand here; PathMacro expands them - // when the copy is actually made. - Assert.Equal(@"\\fileserver\audit\%PCNAME%", config.UploadFileBridge.Destination); - Assert.Equal(100, config.UploadFileBridge.MaxSizeMB); - Assert.Equal([".tmp"], config.UploadFileBridge.BlockedExtensions); - Assert.Equal([".xlsx", ".docx"], config.UploadFileBridge.AllowedExtensions); - } - - // Nothing is copied off the machine unless it was asked for, and an - // empty destination leaves nowhere to copy it to. - [Fact] - public void UploadFileBridgeDefaultsToDisabledWithNoDestination() - { - var config = ConfigLoader.ParseConf("{}"); - - Assert.False(config.UploadFileBridge.Enabled); - Assert.Equal("", config.UploadFileBridge.Destination); - // 0 is no limit, so leaving it out copies whatever it is given. - Assert.Equal(0, config.UploadFileBridge.MaxSizeMB); - // Both empty, so nothing an upload carries goes unrecorded until - // someone says it need not be. - Assert.Empty(config.UploadFileBridge.BlockedExtensions); - Assert.Empty(config.UploadFileBridge.AllowedExtensions); - } - - [Fact] - public void StartupLauncherDefaultsToDisabledWithNoPrograms() - { - var config = ConfigLoader.ParseConf("{}"); - - Assert.False(config.StartupLauncher.Enabled); - Assert.Empty(config.StartupLauncher.Programs); - } - - [Fact] - public void OmittedGroupsFallBackToDefaults() - { - var json = """{ "NetLogger": { "Sender": { "Endpoint": "https://example.com/log" } } }"""; - - var config = ConfigLoader.ParseConf(json); - - Assert.Equal("https://example.com/log", config.NetLogger.Sender.Endpoint); - // Groups that are absent keep their default values. - Assert.False(config.UploadGuard.Enabled); - Assert.Equal(new[] { ".exe", ".bat", ".cmd", ".js", ".vbs" }, config.UploadGuard.BlockedExtensions); - Assert.False(config.SettingPageFilter.Enabled); - Assert.Equal( - new[] { "edge://settings", "edge://flags", "edge://policy" }, - config.SettingPageFilter.BlockedPrefixes); - Assert.False(config.StartupLauncher.Enabled); - Assert.Empty(config.StartupLauncher.Programs); - } - - [Fact] - public void PropertyNamesAreCaseInsensitive() - { - var json = """{ "netlogger": { "sender": { "endpoint": "https://example.com/log" }, "upload": true } }"""; - - var config = ConfigLoader.ParseConf(json); - - Assert.Equal("https://example.com/log", config.NetLogger.Sender.Endpoint); - Assert.True(config.NetLogger.Upload); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - public void EmptyContentReturnsDefaults(string data) - { - var config = ConfigLoader.ParseConf(data); - - Assert.Equal("", config.NetLogger.Sender.Endpoint); - Assert.False(config.UploadGuard.Enabled); - } - } -} +using Xunit; +using BrowserGuard.Configuration; + +namespace BrowserGuard.Tests.Configuration +{ + public class ConfigLoaderTests + { + [Fact] + public void ParsesNestedFeatureGroups() + { + var json = """ + { + "NetLogger": { + "Enabled": false, + "Sender": { "Endpoint": "https://example.com/api/log" }, + "UrlAccess": true, + "Browsing": true, + "Upload": true, + "Download": false, + "Print": true + }, + "UploadGuard": { + "Enabled": false, + "BlockedExtensions": [".exe", ".zip"] + }, + "SettingPageFilter": { + "Enabled": true, + "BlockedPrefixes": ["edge://settings/", "edge://flags/"] + } + } + """; + + var config = ConfigLoader.ParseConf(json); + + Assert.Equal("https://example.com/api/log", config.NetLogger.Sender.Endpoint); + Assert.True(config.NetLogger.UrlAccess); + Assert.False(config.NetLogger.Download); + + Assert.False(config.UploadGuard.Enabled); + Assert.Equal(new[] { ".exe", ".zip" }, config.UploadGuard.BlockedExtensions); + + Assert.True(config.SettingPageFilter.Enabled); + Assert.Equal(new[] { "edge://settings/", "edge://flags/" }, config.SettingPageFilter.BlockedPrefixes); + } + + [Fact] + public void ParsesUploadPathsAndExtensions() + { + var json = """ + { + "UploadGuard": { + "Enabled": true, + "BlockedExtensions": [".exe"], + "AllowedExtensions": [".pdf", ".docx"], + "AllowedPaths": ["^C:\\\\Users\\\\[^\\\\]+\\\\Documents\\\\", "^D:\\\\Share\\\\"], + "BlockedPaths": ["\\\\Confidential\\\\"] + } + } + """; + + var config = ConfigLoader.ParseConf(json); + + Assert.True(config.UploadGuard.Enabled); + Assert.Equal(new[] { ".exe" }, config.UploadGuard.BlockedExtensions); + Assert.Equal(new[] { ".pdf", ".docx" }, config.UploadGuard.AllowedExtensions); + Assert.Equal( + new[] { @"^C:\\Users\\[^\\]+\\Documents\\", @"^D:\\Share\\" }, + config.UploadGuard.AllowedPaths); + Assert.Equal(new[] { @"\\Confidential\\" }, config.UploadGuard.BlockedPaths); + } + + [Fact] + public void UploadPathListsDefaultToEmpty() + { + var config = ConfigLoader.ParseConf("""{ "UploadGuard": { "Enabled": true } }"""); + + // Empty allowed lists mean "no restriction from that rule". + Assert.Empty(config.UploadGuard.AllowedExtensions); + Assert.Empty(config.UploadGuard.AllowedPaths); + Assert.Empty(config.UploadGuard.BlockedPaths); + Assert.Equal(new[] { ".exe", ".bat", ".cmd", ".js", ".vbs" }, config.UploadGuard.BlockedExtensions); + } + + [Fact] + public void ParsesStartupPrograms() + { + var json = """ + { + "StartupLauncher": { + "Enabled": true, + "Programs": [ + { + "Path": "C:\\Program Files\\Contoso\\agent.exe", + "Arguments": ["--mode", "kiosk"], + "WorkingDirectory": "C:\\Program Files\\Contoso", + "EnvironmentVariables": { "CONTOSO_PROFILE": "default", "LANG": "ja" }, + "Sha256": "abc123" + }, + { + "Path": "C:\\Tools\\notify.exe" + } + ] + } + } + """; + + var config = ConfigLoader.ParseConf(json); + + Assert.True(config.StartupLauncher.Enabled); + Assert.Equal(2, config.StartupLauncher.Programs.Length); + + var first = config.StartupLauncher.Programs[0]; + Assert.Equal(@"C:\Program Files\Contoso\agent.exe", first.Path); + Assert.Equal(new[] { "--mode", "kiosk" }, first.Arguments); + Assert.Equal(@"C:\Program Files\Contoso", first.WorkingDirectory); + Assert.Equal("default", first.EnvironmentVariables["CONTOSO_PROFILE"]); + Assert.Equal("ja", first.EnvironmentVariables["LANG"]); + Assert.Equal("abc123", first.Sha256); + + // Everything but the path may be left out. + var second = config.StartupLauncher.Programs[1]; + Assert.Equal(@"C:\Tools\notify.exe", second.Path); + Assert.Empty(second.Arguments); + Assert.Equal("", second.WorkingDirectory); + Assert.Empty(second.EnvironmentVariables); + Assert.Equal("", second.Sha256); + } + + [Fact] + public void ReadsWhereTheLogIsKept() + { + var json = """ + { + "NetLogger": { + "Enabled": true, + "Sender": { + "Enabled": true, + "Endpoint": "https://collector.example.com/log", + "OnSendFailure": { + "SaveLocally": true, + "RetryIntervalMinutes": 15, + "MaxSizeMB": 50 + } + }, + "LocalFile": { + "Enabled": true, + "Directory": "D:\\logs", + "MaxDays": 90, + "MaxSizeMB": 250 + } + } + } + """; + + var config = ConfigLoader.ParseConf(json).NetLogger; + + Assert.True(config.Enabled); + Assert.True(config.LocalFile.Enabled); + Assert.Equal(@"D:\logs", config.LocalFile.Directory); + Assert.Equal(90, config.LocalFile.MaxDays); + Assert.Equal(250, config.LocalFile.MaxSizeMB); + Assert.True(config.Sender.Enabled); + Assert.True(config.Sender.OnSendFailure.SaveLocally); + Assert.Equal(15, config.Sender.OnSendFailure.RetryIntervalMinutes); + Assert.Equal(50, config.Sender.OnSendFailure.MaxSizeMB); + } + + // 0 asks for no limit rather than for a limit of nothing. + [Fact] + public void ReadsAnUnlimitedSpool() + { + var config = ConfigLoader.ParseConf( + """{"NetLogger":{"Sender":{"OnSendFailure":{"SaveLocally":true,"MaxSizeMB":0}}}}"""); + + Assert.Equal(0, config.NetLogger.Sender.OnSendFailure.MaxSizeMB); + } + + // Nothing may be written to this machine unless it was asked for. + [Fact] + public void KeepsNothingLocallyByDefault() + { + var config = ConfigLoader.ParseConf("{}").NetLogger; + + Assert.False(config.Enabled); + Assert.False(config.LocalFile.Enabled); + Assert.False(config.Sender.Enabled); + Assert.False(config.Sender.OnSendFailure.SaveLocally); + Assert.Equal("", config.LocalFile.Directory); + } + + // The files that ship with the installer have to stay loadable. + [Theory] + [InlineData("Resources/BrowserGuard.json")] + [InlineData("BrowserGuard/BrowserGuard.sample.json")] + public void ParsesTheShippedConfigFiles(string relativePath) + { + var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); + var path = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); + Assert.True(File.Exists(path), $"not found: {path}"); + + var config = ConfigLoader.ParseConf(File.ReadAllText(path)); + + // A member that failed to bind would come back as the default, so a + // group is spot checked rather than only asserting no exception. + Assert.NotNull(config.StartupLauncher.Programs); + Assert.NotEmpty(config.SettingPageFilter.BlockedPrefixes); + Assert.NotEqual(0, config.NetLogger.LocalFile.MaxDays); + Assert.NotEqual(0, config.NetLogger.Sender.OnSendFailure.MaxSizeMB); + Assert.NotNull(config.UsageTimeLimit.AllowedTimeRanges); + Assert.NotEqual("", config.UsageTimeLimit.OnExceeded.Action); + } + + // The files the integration test procedure has the tester drop in. A + // procedure whose configuration no longer loads sends them looking for + // a fault in the product. + [Theory] + [InlineData("docs/testconfig/01_all-disabled.json")] + [InlineData("docs/testconfig/02_strict.json")] + public void ParsesTheConfigFilesTheTestProcedureUses(string relativePath) + { + var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); + var path = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); + Assert.True(File.Exists(path), $"not found: {path}"); + + var config = ConfigLoader.ParseConf(File.ReadAllText(path)); + + // Every section has to bind, whether or not that section is on in + // this particular file. + Assert.NotNull(config.NetLogger.LocalFile); + Assert.NotNull(config.UploadGuard.BlockedExtensions); + Assert.NotNull(config.SettingPageFilter.BlockedPrefixes); + Assert.NotNull(config.StartupLauncher.Programs); + Assert.NotNull(config.UsageTimeLimit.AllowedTimeRanges); + Assert.NotNull(config.UploadFileBridge.BlockedUrls); + } + + [Fact] + public void ReadsTheUsageTimeLimit() + { + var json = """ + { + "UsageTimeLimit": { + "Enabled": true, + "MaxContinuousMinutes": 240, + "AllowedTimeRanges": [ + { "Start": "09:00", "End": "12:00" }, + { "Start": "13:00", "End": "18:00" } + ], + "OnExceeded": { + "Action": "Terminate", + "GraceSeconds": 90, + "ReWarnIntervalMinutes": 5 + } + } + } + """; + + var config = ConfigLoader.ParseConf(json); + + Assert.True(config.UsageTimeLimit.Enabled); + Assert.Equal(240, config.UsageTimeLimit.MaxContinuousMinutes); + Assert.Equal(2, config.UsageTimeLimit.AllowedTimeRanges.Length); + Assert.Equal("09:00", config.UsageTimeLimit.AllowedTimeRanges[0].Start); + Assert.Equal("18:00", config.UsageTimeLimit.AllowedTimeRanges[1].End); + Assert.Equal("Terminate", config.UsageTimeLimit.OnExceeded.Action); + Assert.Equal(90, config.UsageTimeLimit.OnExceeded.GraceSeconds); + Assert.Equal(5, config.UsageTimeLimit.OnExceeded.ReWarnIntervalMinutes); + } + + // Nothing may start closing the browser unless it was asked for. + [Fact] + public void UsageTimeLimitDefaultsToDisabledAndWarningOnly() + { + var config = ConfigLoader.ParseConf("{}"); + + Assert.False(config.UsageTimeLimit.Enabled); + Assert.Equal(0, config.UsageTimeLimit.MaxContinuousMinutes); + Assert.Empty(config.UsageTimeLimit.AllowedTimeRanges); + Assert.Equal("WarnOnly", config.UsageTimeLimit.OnExceeded.Action); + } + + [Fact] + public void ReadsTheTabCountLimit() + { + var json = """ + { + "TabCountLimit": { + "Enabled": true, + "MaxCount": 20 + } + } + """; + + var config = ConfigLoader.ParseConf(json); + + Assert.True(config.TabCountLimit.Enabled); + Assert.Equal(20, config.TabCountLimit.MaxCount); + } + + // Zero is "no limit", so a config without a number cannot start warning + // about every tab. + [Fact] + public void TabCountLimitDefaultsToDisabledWithoutALimit() + { + var config = ConfigLoader.ParseConf("{}"); + + Assert.False(config.TabCountLimit.Enabled); + Assert.Equal(0, config.TabCountLimit.MaxCount); + } + + [Fact] + public void ReadsTheUploadFileBridge() + { + var json = """ + { + "UploadFileBridge": { + "Enabled": true, + "Destination": "\\\\fileserver\\audit\\%PCNAME%", + "MaxSizeMB": 100, + "BlockedExtensions": [".tmp"], + "AllowedExtensions": [".xlsx", ".docx"] + } + } + """; + + var config = ConfigLoader.ParseConf(json); + + Assert.True(config.UploadFileBridge.Enabled); + // The macros are left as they stand here; PathMacro expands them + // when the copy is actually made. + Assert.Equal(@"\\fileserver\audit\%PCNAME%", config.UploadFileBridge.Destination); + Assert.Equal(100, config.UploadFileBridge.MaxSizeMB); + Assert.Equal([".tmp"], config.UploadFileBridge.BlockedExtensions); + Assert.Equal([".xlsx", ".docx"], config.UploadFileBridge.AllowedExtensions); + } + + // Nothing is copied off the machine unless it was asked for, and an + // empty destination leaves nowhere to copy it to. + [Fact] + public void UploadFileBridgeDefaultsToDisabledWithNoDestination() + { + var config = ConfigLoader.ParseConf("{}"); + + Assert.False(config.UploadFileBridge.Enabled); + Assert.Equal("", config.UploadFileBridge.Destination); + // 0 is no limit, so leaving it out copies whatever it is given. + Assert.Equal(0, config.UploadFileBridge.MaxSizeMB); + // Both empty, so nothing an upload carries goes unrecorded until + // someone says it need not be. + Assert.Empty(config.UploadFileBridge.BlockedExtensions); + Assert.Empty(config.UploadFileBridge.AllowedExtensions); + } + + [Fact] + public void StartupLauncherDefaultsToDisabledWithNoPrograms() + { + var config = ConfigLoader.ParseConf("{}"); + + Assert.False(config.StartupLauncher.Enabled); + Assert.Empty(config.StartupLauncher.Programs); + } + + [Fact] + public void OmittedGroupsFallBackToDefaults() + { + var json = """{ "NetLogger": { "Sender": { "Endpoint": "https://example.com/log" } } }"""; + + var config = ConfigLoader.ParseConf(json); + + Assert.Equal("https://example.com/log", config.NetLogger.Sender.Endpoint); + // Groups that are absent keep their default values. + Assert.False(config.UploadGuard.Enabled); + Assert.Equal(new[] { ".exe", ".bat", ".cmd", ".js", ".vbs" }, config.UploadGuard.BlockedExtensions); + Assert.False(config.SettingPageFilter.Enabled); + Assert.Equal( + new[] { "edge://settings", "edge://flags", "edge://policy" }, + config.SettingPageFilter.BlockedPrefixes); + Assert.False(config.StartupLauncher.Enabled); + Assert.Empty(config.StartupLauncher.Programs); + } + + [Fact] + public void PropertyNamesAreCaseInsensitive() + { + var json = """{ "netlogger": { "sender": { "endpoint": "https://example.com/log" }, "upload": true } }"""; + + var config = ConfigLoader.ParseConf(json); + + Assert.Equal("https://example.com/log", config.NetLogger.Sender.Endpoint); + Assert.True(config.NetLogger.Upload); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void EmptyContentReturnsDefaults(string data) + { + var config = ConfigLoader.ParseConf(data); + + Assert.Equal("", config.NetLogger.Sender.Endpoint); + Assert.False(config.UploadGuard.Enabled); + } + } +} diff --git a/docs/testconfig/01_all-disabled.json b/docs/testconfig/01_all-disabled.json new file mode 100644 index 0000000..8dbd880 --- /dev/null +++ b/docs/testconfig/01_all-disabled.json @@ -0,0 +1,64 @@ +{ + "NetLogger": { + "Enabled": false, + "UrlAccess": false, + "Browsing": false, + "Upload": false, + "Download": false, + "Print": false, + "LocalFile": { + "Enabled": false, + "Directory": "", + "MaxDays": 30, + "MaxSizeMB": 0 + }, + "Sender": { + "Enabled": false, + "Endpoint": "", + "OnSendFailure": { + "SaveLocally": false, + "RetryIntervalMinutes": 5, + "MaxSizeMB": 10 + } + } + }, + "UploadGuard": { + "Enabled": false, + "BlockedExtensions": [], + "AllowedExtensions": [], + "AllowedPaths": [], + "BlockedPaths": [] + }, + "SettingPageFilter": { + "Enabled": false, + "NotifyOnBlocked": true, + "BlockedPrefixes": [] + }, + "StartupLauncher": { + "Enabled": false, + "Programs": [] + }, + "UsageTimeLimit": { + "Enabled": false, + "MaxContinuousMinutes": 0, + "AllowedTimeRanges": [], + "OnExceeded": { + "Action": "WarnOnly", + "GraceSeconds": 60, + "ReWarnIntervalMinutes": 10 + } + }, + "TabCountLimit": { + "Enabled": false, + "MaxCount": 0 + }, + "UploadFileBridge": { + "Enabled": false, + "Destination": "", + "MaxSizeMB": 0, + "BlockedExtensions": [], + "AllowedExtensions": [], + "BlockedUrls": [], + "AllowedUrls": [] + } +} diff --git a/docs/testconfig/02_strict.json b/docs/testconfig/02_strict.json new file mode 100644 index 0000000..7ea56dc --- /dev/null +++ b/docs/testconfig/02_strict.json @@ -0,0 +1,75 @@ +{ + "NetLogger": { + "Enabled": true, + "UrlAccess": true, + "Browsing": true, + "Upload": true, + "Download": true, + "Print": true, + "LocalFile": { + "Enabled": true, + "Directory": "C:\\BrowserGuardTest\\netlog\\%PCNAME%", + "MaxDays": 30, + "MaxSizeMB": 0 + }, + "Sender": { + "Enabled": true, + "Endpoint": "http://localhost:8001", + "OnSendFailure": { + "SaveLocally": true, + "RetryIntervalMinutes": 1, + "MaxSizeMB": 10 + } + } + }, + "UploadGuard": { + "Enabled": true, + "BlockedExtensions": [".exe", ".bat", ".cmd", ".js", ".vbs"], + "AllowedExtensions": [".pdf", ".docx", ".xlsx"], + "AllowedPaths": ["^C:\\\\Users\\\\[^\\\\]+\\\\Documents\\\\"], + "BlockedPaths": ["\\\\Confidential\\\\"] + }, + "SettingPageFilter": { + "Enabled": true, + "NotifyOnBlocked": true, + "BlockedPrefixes": ["edge://settings", "edge://flags", "edge://policy", "edge://extensions"] + }, + "StartupLauncher": { + "Enabled": true, + "Programs": [ + { + "Path": "C:\\BrowserGuardTest\\EnvDump.exe", + "Arguments": [], + "WorkingDirectory": "C:\\BrowserGuardTest", + "EnvironmentVariables": { "BROWSERGUARD_STARTUP_TEST": "from-config" }, + "Sha256": "" + } + ] + }, + "UsageTimeLimit": { + "Enabled": true, + "MaxContinuousMinutes": 5, + "AllowedTimeRanges": [ + { "Start": "09:00", "End": "12:00" }, + { "Start": "13:00", "End": "18:00" } + ], + "OnExceeded": { + "Action": "Terminate", + "GraceSeconds": 60, + "ReWarnIntervalMinutes": 1 + } + }, + "TabCountLimit": { + "Enabled": true, + "MaxCount": 5 + }, + "UploadFileBridge": { + "Enabled": true, + "Destination": "C:\\BrowserGuardTest\\audit\\%PCNAME%\\%USERID%\\%DATE%", + "MaxSizeMB": 10, + "BlockedExtensions": [".tmp", ".log"], + "AllowedExtensions": [], + "BlockedUrls": ["^https://localhost"], + "AllowedUrls": [] + } +} diff --git "a/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/MiXedCase_Sample.TxT" "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/MiXedCase_Sample.TxT" new file mode 100644 index 0000000..84c77c9 --- /dev/null +++ "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/MiXedCase_Sample.TxT" @@ -0,0 +1 @@ +BrowserGuard integration test sample (mixed case). diff --git "a/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\206\343\202\271\343\203\210\350\263\207\346\226\231+\346\224\271\350\250\202-\347\254\2542\347\211\210 (\346\234\200\347\265\202)#1.TxT" "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\206\343\202\271\343\203\210\350\263\207\346\226\231+\346\224\271\350\250\202-\347\254\2542\347\211\210 (\346\234\200\347\265\202)#1.TxT" new file mode 100644 index 0000000..5a6861a --- /dev/null +++ "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\206\343\202\271\343\203\210\350\263\207\346\226\231+\346\224\271\350\250\202-\347\254\2542\347\211\210 (\346\234\200\347\265\202)#1.TxT" @@ -0,0 +1 @@ +BrowserGuard 連結テスト用サンプル(日本語・記号)。 diff --git "a/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\221\343\203\224\343\203\227\343\203\232\343\203\235_\343\202\254\343\202\256\343\202\260\343\202\262\343\202\264.TxT" "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\221\343\203\224\343\203\227\343\203\232\343\203\235_\343\202\254\343\202\256\343\202\260\343\202\262\343\202\264.TxT" new file mode 100644 index 0000000..e148479 --- /dev/null +++ "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\343\203\221\343\203\224\343\203\227\343\203\232\343\203\235_\343\202\254\343\202\256\343\202\260\343\202\262\343\202\264.TxT" @@ -0,0 +1 @@ +BrowserGuard 連結テスト用サンプル(濁点・半濁点)。 diff --git "a/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\345\243\262\344\270\212&\347\265\214\350\262\273_100%\351\201\224\346\210\220.CsV" "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\345\243\262\344\270\212&\347\265\214\350\262\273_100%\351\201\224\346\210\220.CsV" new file mode 100644 index 0000000..faa69c5 --- /dev/null +++ "b/docs/testfiles/\346\244\234\350\250\274\347\224\250+\343\203\225\343\202\251\343\203\253\343\203\200\343\203\274-A/\345\243\262\344\270\212&\347\265\214\350\262\273_100%\351\201\224\346\210\220.CsV" @@ -0,0 +1,3 @@ +項目,金額 +売上,100 +経費,50 diff --git "a/docs/\351\200\243\347\265\220\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206\346\233\270.md" "b/docs/\351\200\243\347\265\220\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206\346\233\270.md" new file mode 100644 index 0000000..85b6c5a --- /dev/null +++ "b/docs/\351\200\243\347\265\220\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206\346\233\270.md" @@ -0,0 +1,1240 @@ +--- +CJKmainfont: Noto Sans CJK JP +CJKoptions: + - BoldFont=Noto Sans CJK JP Bold +title: | + BrowserGuard + 連結テスト手順書 +subject: BrowserGuard 連結テスト手順書 +date: 2026/08/21 +author: 株式会社クリアコード +keywords: [BrowserGuard, Test Document, Integration Test] +titlepage: true +toc-title: 目次 +toc-own-page: true +--- + +# BrowserGuard 連結テスト手順書 + +拡張機能・ネイティブメッセージングホスト・インストーラーを組み合わせた状態で、 +機能が意図どおり連動することを確認する。 + +単体テスト(`dotnet test` および `make.bat test`)で検証済みの判定ロジックは対象としない。 +本書は、**プロセスをまたぐ経路が実際につながっていること**を対象とする。 + +### 確認の記号 + +* `* [ ]` … 実施者がチェックする確認項目 +* `■` … 補足・注意事項 + +## テスト環境 + +### 対象構成 + +1. Microsoft Edge(Chromium 版) +2. BrowserGuard 拡張機能(強制インストール) +3. BrowserGuard ネイティブメッセージングホスト +4. 設定ファイル `BrowserGuard.json` + +### 事前準備 + +* [ ] テスト用の `BrowserGuardSetup.exe` を準備する +* [ ] テスト対象のマシンに BrowserGuard がインストールされて**いない**こと +* [ ] `HKLM\SOFTWARE\Policies\Microsoft\Edge` の `ExtensionSettings` の現在値を控えておく + ``` + reg query "HKLM\SOFTWARE\Policies\Microsoft\Edge" + ``` + ■ グループポリシーで `ExtensionSettings` が構成されている環境では、`gpupdate` のたびに + グループポリシー側の値で上書きされる。実際に適用されている内容は `edge://policy` で確認する。 +* テスト用のサーバーを起動する + * ■ コレクター用のサーバーとアップロード用のサーバーはそれぞれ別の PowerShell ウィンドウで実行し、 + 本書の確認中は起動したままにする。 + * [ ] コレクター確認用のサーバー(`http://localhost:8001`)を起動する + ``` + powershell -ExecutionPolicy Bypass -File tools\httpserver.ps1 + ``` + * [ ] `Running File Uploading Server: http://localhost:8000` と表示されること + * ■ 受け取った要求のメソッド・URL・ヘッダーと、POST の本文を画面に表示する。 + 監査ログの送信内容は、このウィンドウで確認する。 + * [ ] アップロード確認用のサーバー(`http://localhost:8000`)を起動する + ``` + powershell -ExecutionPolicy Bypass -File tools\uploadserver.ps1 + ``` + ■ `uploadserver.ps1` は、`http://localhost:8000` を開くとファイル選択フォームを返し、 + 送信されたファイルを受け取って `UPLOADED: ...` と表示する。 + 本書で「アップロードする」とあるものは、このフォームからの送信を指す。 + + +### Edgeの起動方法 + +本手順書の確認中は、Edgeの起動時、拡張機能のログが出るように、常にEdgeのログを有効にして起動する。 + +1. Edgeのプロセスを完全に終了する + ``` + taskkill /F /IM msedge.exe /T + ``` + ■ この手順を実施しないと、残存していた前回のプロセスの影響で、ログが出なくなることがある +2. ログを有効にしてEdgeを起動する + ``` + "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --enable-logging --v=1 + ``` +3. 以下のパスにEdgeのログが出力される + * `%LOCALAPPDATA%\Microsoft\Edge\User Data\chrome_debug.log` + +■ `chrome_debug.log` は Edge の起動ごとに作り直されるため、前回起動時の内容は残らない。 + +### 参照先 + +以下を随時参照する。 + +* ホストのログ … `%APPDATA%\BrowserGuard\BrowserGuard.log` +* 監査ログ … `%LocalAppData%\BrowserGuard\netlog\netlog.jsonl`(既定値の場合) +* 設定ファイル … `%ProgramFiles%\BrowserGuard\BrowserGuard.json` +* 拡張機能のログ … `%LOCALAPPDATA%\Microsoft\Edge\User Data\chrome_debug.log` + (Edge を `--enable-logging --v=1` 付きで起動した場合) + +■ 設定ファイルの位置は、レジストリ `HKLM\SOFTWARE\BrowserGuard` の `Configfile` が指す先である。 + インストール先を変更した場合はそちらを参照する。 + +■ ポリシーで強制インストールした拡張機能は既定では開発者ツールを開けないため、 + 拡張機能のログは開発者ツールのコンソールではなく `chrome_debug.log` から確認する。 + Edge の起動方法は「疎通確認」を参照する。 + +### 設定変更の反映手順 + +各項目で設定を変更する場合、以下の手順で反映させる。 + +* 設定ファイルを保存する +* Edge を再起動する + +■ 設定は拡張機能のサービスワーカー起動時に読み込まれる。 + サービスワーカーは待機状態から復帰するたびに読み直すため、多くの場合は再起動なしでも反映されるが、 + 確実を期すため各項目では再起動を前提とする。 + +## インストール + +### インストーラーの実行 + +■ インストーラーは開始時に `msedge.exe` と `BrowserGuard.exe` を強制終了する。 + 実行前に、保存していない作業がないことを確認する。 + +1. `SetupOutput\BrowserGuardSetup.exe` を管理者権限で実行する + * [ ] 「拡張機能を Edge のポリシーに登録して自動的にインストールする」タスクが表示されること + * [ ] **既定でチェックが外れていること** + * [ ] グループポリシーが優先される旨の注意書きが表示されること +3. チェックを入れずにインストールを完了する + +■ 既定でオフなのは、グループポリシーで管理する運用を標準とし、 + レジストリへの直接書き込みは明示的に選択した場合のみ行うためである。 + +### 配置されるもの + +インストール先(既定では `%ProgramFiles%\BrowserGuard`)を参照する。 + +* [ ] `BrowserGuard.json` が配置されていること +* [ ] `BrowserGuardHost\BrowserGuard.exe` が配置されていること +* [ ] `BrowserGuardHost\edge.json`(ホストマニフェスト)が配置されていること +* [ ] `BrowserGuardExtension\BrowserGuardEdge.crx` が配置されていること +* [ ] `BrowserGuardExtension\manifest.xml`(更新マニフェスト)が配置されていること + +### レジストリ + +■ 以下の期待値は、既定のインストール先 `C:\Program Files\BrowserGuard` を前提とする。 + インストール先を変更した場合は、そのパスに読み替える。 + +■ 値はすべて文字列型(`REG_SZ`)である。 +■ レジストリエディター(タスクバーの検索欄に「regedit」と入力して起動)でも同じ内容を確認できる。 + +1. 管理者権限でコマンドプロンプトを起動し、次を実行して結果を控える: + ``` + reg query "HKLM\SOFTWARE\BrowserGuard" + reg query "HKLM\SOFTWARE\Microsoft\Edge\NativeMessagingHosts\com.clear_code.browser_guard" + reg query "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v ExtensionSettings + reg query "HKLM\SOFTWARE\WOW6432Node\BrowserGuard" + ``` +2. `HKLM\SOFTWARE\BrowserGuard` を確認する: + * [ ] `Path` が `C:\Program Files\BrowserGuard\` であること(末尾の `\` を含む) + * [ ] `ClientType` が存在し、値が空文字列であること + * [ ] `Version` が `1.0.0.0` であること + * [ ] `Configfile` が `C:\Program Files\BrowserGuard\BrowserGuard.json` であること + * [ ] `ExtensionExecfile` が `C:\Program Files\BrowserGuard\BrowserGuardHost\BrowserGuard.exe` であること + * [ ] `ExtensionSettingsRegistered` が**存在しない**こと + * ■ インストーラー自身がポリシーを書いたことを記録する値。ここではタスクにチェックを入れていないため、書かれない。 + * [ ] 上記以外の値が作られていないこと +3. `HKLM\SOFTWARE\WOW6432Node\BrowserGuard` を確認する: + * [ ] キーが存在しないこと + (`reg query` が「指定されたレジストリ キーまたは値が見つかりませんでした」を返すこと) + * ■ ホストは `win-x64` として発行され、64 ビットプロセスとして動作するため、 + `HKLM\SOFTWARE\BrowserGuard` をそのまま読む。 + 32 ビットビュー(`WOW6432Node`)への複製は不要であり、インストーラーも書き込まない。 + ここにキーが残っている場合は、以前の 32 ビット版の残骸か、他の製品が作ったものである。 +4. 値が指す先を確認する: + * [ ] `Configfile` が指すパスに `BrowserGuard.json` が実在すること + * [ ] `ExtensionExecfile` が指すパスに `BrowserGuard.exe` が実在すること +5. `HKLM\SOFTWARE\Microsoft\Edge\NativeMessagingHosts\com.clear_code.browser_guard` を確認する: + * [ ] キーが存在すること + * [ ] 既定値(`(既定)` / `(Default)`)が + `C:\Program Files\BrowserGuard\BrowserGuardHost\edge.json` であること + * [ ] そのパスに `edge.json` が実在すること + * [ ] 既定値以外の、名前付きの値が無いこと +6. `HKLM\SOFTWARE\Policies\Microsoft\Edge` を確認する: + * [ ] `ExtensionSettings` が、「事前準備」で控えた値から変化していないこと + * [ ] 控えた時点で `ExtensionSettings` が無かった場合、いまも存在しないこと + (`reg query` が「指定されたレジストリ キーまたは値が見つかりませんでした」を返すこと) + +### ホストマニフェスト + +`BrowserGuardHost\edge.json` を参照する。 + +* [ ] `path` が配置された `BrowserGuard.exe` を指すこと +* [ ] `allowed_origins` に拡張機能 ID が含まれること +* [ ] その ID が `BrowserGuardExtension\manifest.xml` の `appid` と一致すること + +■ ID は署名鍵から決まる。ここが食い違うと、拡張機能はインストールされてもホストと通信できない。 + +### ポリシー登録なしの状態(上記のインストールの状態) + +1. Edge を起動する: + * [ ] 拡張機能がインストールされて**いない**こと + * [ ] ブラウザーが通常どおり動作すること + +■ crx を配置しただけでは Edge は拡張機能をインストールしない。ポリシーへの登録が別途必要である。 + +### ポリシー登録あり + +1. インストーラーを再実行する +2. 「拡張機能を Edge のポリシーに登録して自動的にインストールする」タスクにチェックを入れてインストールする +3. `HKLM\SOFTWARE\BrowserGuard` を参照する: + * 管理者権限でコマンドプロンプトを実行し、`reg query "HKLM\SOFTWARE\BrowserGuard"`を実行する + * [ ] `ExtensionSettingsRegistered` が記録されていること + * ■ `ExtensionSettings` は全拡張機能を 1 つの JSON 値で表す。 + 既存の内容を読み込んで自分の項目だけを足すため、他製品の設定は残る。 +4. Edge を起動する: + * [ ] 拡張機能がインストールされること + * [ ] `edge://extensions` に表示され、有効であること + * [ ] 削除・無効化ができないこと(ポリシーによる強制インストール) +5. `edge://policy` を参照する: + * [ ] `ExtensionSettings` が適用されていること + + ■ 強制インストールされた拡張機能は、Edge が更新チェックを行ったときに入れ替わる。 + すぐに反映させるには `edge://extensions` で開発者モードをオンにし、[更新]を押す。 + +### 設定ファイルの上書き + +1. `BrowserGuard.json` を編集し、任意の設定を変更する +2. インストーラーを再実行する: + * [ ] **編集した内容が保持されていること** + +■ 設定ファイルは `onlyifdoesntexist` で配置される。アップグレード時に設定が失われないための扱いである。 + +### サイレントインストール + +1. アンインストール後、次を実行する: + +``` +BrowserGuardSetup.exe /VERYSILENT +``` + + * [ ] 画面を表示せずインストールされること + * [ ] `ExtensionSettings` が変更されて**いない**こと + +2. アンインストール後、次を実行する: + +``` +SetupOutput\BrowserGuardSetup.exe /VERYSILENT /TASKS="extensionpolicy" +``` + + * [ ] `ExtensionSettings` に登録されること + +## 疎通確認 + +### 拡張機能とホストの通信 + +設定ファイルの `NetLogger.Enabled` を `true`、`LocalFile.Enabled` を `true` にする。 + +1. 「Edgeの起動方法」の手順でEdgeを再起動する +2. 任意のページを表示する +3. `%LOCALAPPDATA%\Microsoft\Edge\User Data\chrome_debug.log` を参照する + * [ ] 次の形式の行が記録されていること + ``` + "Fetch config {"NetLogger":{...}, ...}", source: chrome-extension://<拡張機能 ID>/config-loader.js (23) + ``` + * [ ] `Cannot fetch config` の行が**無い**こと + * [ ] `source:` に含まれる `<拡張機能 ID>` が + `BrowserGuardExtension\manifest.xml` の `appid` と一致すること + * [ ] `Fetch config` に続く JSON が、設定ファイル `BrowserGuard.json` の内容と一致していること +4. `%APPDATA%\BrowserGuard\BrowserGuard.log` を参照する: + * [ ] `Command: load config` が記録されていること + +■ 設定ファイルに書かなかったキーは、ホスト側の既定値で埋められた状態で出力される。 + そのため、出力される JSON は設定ファイルより項目数が多くなる。 +■ `Fetch config` が記録されない場合、または `Cannot fetch config` が記録される場合、 + 以降のすべての項目が失敗する。ホストマニフェストの登録とレジストリの `Configfile` を先に確認する。 + +### ホストが起動できない場合の挙動 + +1. レジストリ `HKLM\SOFTWARE\BrowserGuard` の `Configfile` を存在しないパスに変更する +2. 「Edgeの起動方法」の手順でEdgeを再起動する + * [ ] ブラウザーの操作が阻害されないこと + * [ ] `chrome_debug.log` に `"Fetch config undefined"` の行が記録されること(設定を取得できていない) + * [ ] `%APPDATA%\BrowserGuard\BrowserGuard.log` に、変更先のパスが見つからない旨が記録されること +3. `Configfile` を元に戻し、「Edgeの起動方法」の手順で Edge を再起動する + * [ ] 通常どおり動作すること + +## 設定値ごとの確認 + +すべての設定値について、次の 2 つを確認する。 + +1. (デフォルト値)その値のときの挙動 +2. (変更時)値を変えたときに挙動が変わること + +■ 組み合わせの網羅(禁止リストが許可リストに勝つか、大文字小文字を区別するか等)は単体テストで確認済みのため、ここでは扱わない。 + 本節の目的は、**設定した値が実際に挙動へ届いていること**の確認である。設定の受け渡しはホストと拡張機能をまたぐため、この経路は単体テストでは確認できない。 + +### デフォルト値について + +「デフォルト値」には 2 種類あり、本節では前者を指す。 + +* **キーを書かなかった場合の値**(プログラムが持つ既定値) +* インストーラーが配置する `BrowserGuard.json` に書かれている値 + +■ キーを省略した場合の確認は、そのキーの行を設定ファイルから削除して行う。 + **省略した結果、遮断や記録が黙って無効になっていないか**が要点である。 + +## 監査ログ(NetLogger) + +■ 以下で「ページを表示する」とある手順は、`Browsing` が `true` でないと何も記録されない。 + 操作の種別ごとの有効・無効は「UrlAccess / Browsing / Upload / Download / Print」で扱うため、 + 同項を除き、`Browsing` は `true` にしたうえで確認する。 + +#### NetLogger.Enabled + +1. (デフォルト値 `false`)`Browsing` を `true` にして任意のページを表示する: + * [ ] 監査ログのファイルが作成されないこと +2. (変更時 `true`)`LocalFile.Enabled` も `true` にして任意のページを表示する: + * [ ] 監査ログが記録されること + +#### NetLogger.Sender.Enabled + +前提:`Browsing` を `true`、`Sender.Endpoint` に `http://localhost:8001` を設定しておく。 + +1. (デフォルト値 `false`)ページを表示する: + * [ ] `httpserver.ps1` のウィンドウに何も表示されないこと + * [ ] ローカルファイルへの記録は行われること +2. (変更時 `true`)ページを表示する: + * [ ] `httpserver.ps1` のウィンドウに `POST: /` と本文が表示されること + +#### NetLogger.Sender.Endpoint + +前提:`Browsing` を `true`、`Sender.Enabled` を `true` にしておく。 + +1. (デフォルト値 `""`)ページを表示する: + * [ ] `httpserver.ps1` のウィンドウに何も表示されないこと + * [ ] ローカルファイルへの記録は行われること +2. (変更時 `http://localhost:8001`)ページを表示する: + * [ ] `httpserver.ps1` のウィンドウに `POST: /` と本文が表示されること + +■ コレクターへの送信は、`Sender.Enabled` が `true` で、かつ `Sender.Endpoint` が + 空でない場合にのみ行われる。片方だけでは送信されない。 + +#### NetLogger.Sender.OnSendFailure.SaveLocally + +前提:`Browsing` を `true`、`Sender.Enabled` を `true`、`Sender.Endpoint` を設定し、 +`httpserver.ps1` を Ctrl+C で停止しておく。 + +1. (デフォルト値 `false`)ページを表示する: + * [ ] `netlog-pending.jsonl` が作成され**ない**こと +2. (変更時 `true`)ページを表示する: + * [ ] `netlog-pending.jsonl` に保持されること + +#### NetLogger.Sender.OnSendFailure.RetryIntervalMinutes + +1. (デフォルト値 `5`) + * `httpserver.ps1` を Ctrl+C で停止しておく。 + * ページを表示する + * `httpserver.ps1` を起動する: + * [ ] 5 分以内に再送されること +2. (変更時 `1`)同様に確認する: + * [ ] 1 分程度で再送されること + +■ `0` を指定した場合は再送を行わない。手作業での回収を想定した設定である。 + +#### NetLogger.Sender.OnSendFailure.MaxSizeMB + +1. (デフォルト値 `10`)`httpserver.ps1` を停止したまま記録を続ける: + * [ ] 10 MB 付近で保持が頭打ちになること +2. (変更時 `1`)同様に確認する: + * [ ] 1 MB 付近で頭打ちになること + +#### NetLogger.LocalFile.Enabled + +前提:`Browsing` を `true`、`Sender.Enabled` を `true`、 +`Sender.Endpoint` に `http://localhost:8001` を設定し、`httpserver.ps1` を起動しておく。 + +1. (デフォルト値 `false`)`NetLogger.Enabled` を `true` にしてページを表示する: + * [ ] ローカルファイルが作成され**ない**こと + * [ ] `httpserver.ps1` のウィンドウには `POST: /` と本文が表示されること +2. (変更時 `true`)Edge を再起動し、同じ操作を行う: + * [ ] `netlog.jsonl` に記録されること + +■ ローカルファイルとコレクターは互いに独立した出力先である。 + 片方を無効にしても、もう片方への出力は行われる。 + +#### NetLogger.LocalFile.Directory + +1. (デフォルト値 `""`)ページを表示する: + * [ ] `%LocalAppData%%\BrowserGuard\netlog` に出力されること +2. (変更時 任意のフォルダー)ページを表示する: + * [ ] 指定したフォルダーに出力されること + * [ ] フォルダーが存在しない場合は作成されること + +■ %LocalAppData%\BrowserGuard\netlogは、`Sender.OnSendFailure.SaveLocally` による + `netlog-pending.jsonl` の置き場所でもある。`LocalFile.Enabled` が `false` のときも、保持先はこの設定に従う。 + +#### NetLogger.LocalFile.MaxDays + +手順は「ローテーション」の「保持期間による削除」で行う。 + +1. (デフォルト値 `30`) + * [ ] 30 日前の日付のファイルが残ること + * [ ] 31 日前の日付のファイルが削除されること +2. (変更時 `1`)同じ手順を `MaxDays` = `1` で行う: + * [ ] 1 日前の日付のファイルが残ること + * [ ] 2 日前の日付のファイルが削除されること + +■ `0` を指定した場合は削除を行わない。 + +#### NetLogger.LocalFile.MaxSizeMB + +1. (デフォルト値 `0`)1MB以上記録する: + * [ ] サイズによる分割が行われないこと +2. (変更時 `1`)1MBを超えるまで記録する: + * [ ] 同一日のファイルが分割されること + +#### UrlAccess / Browsing / Upload / Download / Print + +前提:`NetLogger.Enabled` と `NetLogger.Sender.Enabled` と `NetLogger.LocalFile.Enabled` を `true` にする。 +記録の有無は `netlog.jsonl` の `operation` で判定する。 + +各キーについて、次の操作を行って確認する。 + +1. (デフォルト値 `false`)そのキーに対応する操作を行う: + * [ ] 対応する `operation` のエントリが記録され**ない**こと +2. (変更時 `true`)Edge を再起動し、同じ操作を行う: + * [ ] 対応する `operation` のエントリが記録されること + * [ ] `name` と `url` が、各キーの項に記載のとおりであること + +■ 記録は `operation` で見分けられるため、5 つのキーをまとめて確認してもよい。 + その場合、すべて `false` の状態で 5 つの操作を一通り行って記録されないことを確認し、 + すべて `true` にして同じ操作を繰り返す。 + +##### `UrlAccess`(`operation` = `urlaccess`) + +1. `http://localhost:8000` を開く + * [ ] `name` が `localhost`、`url` が `http://localhost:8000/` であること + +■ ページ内のすべての http/https 要求が記録される。 + 1 回の表示で複数のエントリが記録されることがある。 + +##### `Browsing`(`operation` = `browsing`) + +1. `http://localhost:8000` を開く + * [ ] `name` がページのタイトル、`url` が `http://localhost:8000/` であること + +■ 記録されるのはタブ自身の遷移だけである。ページ内のフレームの読み込みでは記録されない。 + +##### `Upload`(`operation` = `upload`) + +1. `http://localhost:8000` を開く +2. フォームで任意の `.txt` ファイルを選び、[Submit] を押す + * [ ] `name` が送信したファイル名、`url` が `http://localhost:8000/` であること + +■ ファイル名の大文字・小文字が元のとおりであることは、 + 「記録される文字列の大文字・小文字」で確認する。 + +##### `Download`(`operation` = `download`) + +1. `http://localhost:8000` を開く +2. Ctrl+S を押し、`C:\BrowserGuardTest\` へページを保存する + * [ ] `name` が保存先のフルパス、`url` が `http://localhost:8000/` であること + +■ ファイル名の大文字・小文字が元のとおりであることは、 + 「記録される文字列の大文字・小文字」で確認する。 + +■ 任意のサイトからファイルをダウンロードしても同じように記録される。 + 記録されるのはダウンロードが完了した時点である。 + +##### `Print`(`operation` = `print`) + +1. `http://localhost:8000` を開く +2. Ctrl+P を押して印刷プレビューを開く + * [ ] `name` がページのタイトル、`url` が `http://localhost:8000/` であること + +■ 印刷プレビューを開いた時点で記録される。実際に印刷する必要はない。 + +■ 遮断や失敗の記録(`upload-guard`、`upload-file-bridge`)は、これらの設定に関わらず行われる。 + +### UploadGuard + +前提:アップロードは `http://localhost:8000`(`uploadserver.ps1`)のフォームから行う。 + +#### Enabled + +1. (デフォルト値 `false`)`.exe` ファイルをアップロードする: + * [ ] 遮断され**ない**こと +2. (変更時 `true`)同じ操作を行う: + * [ ] 遮断されること + +#### BlockedExtensions + +1. (デフォルト値 `[".exe", ".bat", ".cmd", ".js", ".vbs"]`)`.exe` をアップロードする: + * [ ] 遮断されること + * [ ] `.txt` は遮断されないこと +2. (変更時 `[".txt"]`)`.txt` をアップロードする: + * [ ] 遮断されること + * [ ] `.exe` が遮断され**ない**こと + +■ 2 で `.exe` が遮断されないことまで確認する。既定値が残ったまま上書きされていない状態を検出するため。 + +#### AllowedExtensions + +1. (デフォルト値 `[]`)任意の拡張子をアップロードする: + * [ ] この規則による遮断が行われないこと +2. (変更時 `[".pdf"]`)`.txt` をアップロードする: + * [ ] 遮断されること + * [ ] `.pdf` は遮断されないこと + +#### AllowedPaths + +1. (デフォルト値 `[]`)任意の場所のファイルをアップロードする: + * [ ] この規則による遮断が行われないこと +2. (変更時 `["^C:\\\\Users\\\\[^\\\\]+\\\\Documents\\\\"]`)`Documents` 外のファイルをアップロードする: + * [ ] 遮断されること + * [ ] `Documents` 配下は遮断されないこと + +#### BlockedPaths + +1. (デフォルト値 `[]`)任意の場所のファイルをアップロードする: + * [ ] この規則による遮断が行われないこと +2. (変更時 `["\\\\Confidential\\\\"]`)`Confidential` 配下のファイルをアップロードする: + * [ ] 遮断されること + +### SettingPageFilter + +#### Enabled + +1. (デフォルト値 `false`)`edge://settings` を開く: + * [ ] 表示できること +2. (変更時 `true`)同じ操作を行う: + * [ ] 遮断されること + +#### NotifyOnBlocked + +■ キー省略時は `false`、配置される設定ファイルでは `true`。両方確認する。 + +1. (キー省略 = `false`)遮断されるアドレスを開く: + * [ ] ダイアログが表示され**ない**こと + * [ ] 遮断自体は行われること +2. (変更時 `true`)同じ操作を行う: + * [ ] ダイアログが表示されること + +#### BlockedPrefixes + +1. (デフォルト値)`edge://settings`・`edge://flags`・`edge://policy` を開く: + * [ ] いずれも遮断されること + * [ ] `edge://version` は表示できること +2. (変更時 `["edge://version"]`)`edge://version` を開く: + * [ ] 遮断されること + * [ ] `edge://settings` が遮断され**ない**こと + +### StartupLauncher + +#### Enabled + +1. (デフォルト値 `false`)`Programs` を設定した状態で Edge を起動する: + * [ ] プログラムが起動し**ない**こと +2. (変更時 `true`)Edge を起動する: + * [ ] プログラムが起動すること + +#### Programs + +1. (デフォルト値 `[]`)Edge を起動する: + * [ ] 何も起動されないこと + * [ ] ホストのログにエラーが記録されないこと +2. (変更時 1 件設定)Edge を起動する: + * [ ] 指定したプログラムが起動すること +3. (変更時 2 件設定)Edge を起動する: + * [ ] 両方が起動すること + +#### Programs[].Arguments / WorkingDirectory / EnvironmentVariables + +1. (デフォルト値 いずれも空)Edge を起動する: + * [ ] 引数なしで起動すること + * [ ] 作業フォルダーが実行ファイルの場所であること +2. (変更時)それぞれ値を設定して Edge を起動する: + * [ ] 指定した引数が渡ること + * [ ] 指定した作業フォルダーで起動すること + * [ ] 指定した環境変数が渡ること + +#### Programs[].Sha256 + +1. (デフォルト値 `""`)Edge を起動する: + * [ ] ハッシュ検証を行わず起動すること +2. (変更時 正しい値)Edge を起動する: + * [ ] 起動すること +3. (変更時 誤った値)Edge を起動する: + * [ ] 起動し**ない**こと + * [ ] ホストのログに不一致が記録されること + +### UsageTimeLimit + +#### Enabled + +1. (デフォルト値 `false`)`MaxContinuousMinutes` を `1` にして待つ: + * [ ] 警告が表示され**ない**こと +2. (変更時 `true`)同様に待つ: + * [ ] 警告が表示されること + +#### MaxContinuousMinutes + +1. (デフォルト値 `0`)長時間使用する: + * [ ] 連続使用時間による警告が出ないこと +2. (変更時 `1`)1 分以上使用する: + * [ ] 警告が表示されること + +#### AllowedTimeRanges + +1. (デフォルト値 `[]`)任意の時刻に使用する: + * [ ] 時間帯による警告が出ないこと +2. (変更時 現在時刻を含まない時間帯)Edge を起動する: + * [ ] 警告が表示されること +3. (変更時 現在時刻を含む時間帯)Edge を起動する: + * [ ] 警告が表示され**ない**こと + +#### OnExceeded.Action + +1. (デフォルト値 `WarnOnly`)上限を超える: + * [ ] 警告のみで、ブラウザーが終了し**ない**こと +2. (変更時 `Terminate`)上限を超える: + * [ ] 猶予時間の経過後、すべてのウィンドウが閉じられること + +■ `WarnOnly` と `Terminate` 以外を書いた場合は `WarnOnly` として扱われる。誤記で終了が始まらないための扱いである。 + +#### OnExceeded.GraceSeconds + +1. (デフォルト値 `60`)`Terminate` で上限を超える: + * [ ] ダイアログに約 1 分後の時刻が表示されること + * [ ] その時刻に終了すること +2. (変更時 `180`)同様に確認する: + * [ ] 約 3 分後の時刻が表示され、その時刻に終了すること + +#### OnExceeded.ReWarnIntervalMinutes + +1. (デフォルト値 `10`)`WarnOnly` で上限を超えた状態を保つ: + * [ ] 10 分ごとに再度警告されること +2. (変更時 `1`)同様に確認する: + * [ ] 1 分ごとに警告されること +3. (変更時 `0`)同様に確認する: + * [ ] 再警告され**ない**こと + +### TabCountLimit + +#### Enabled + +1. (デフォルト値 `false`)`MaxCount` を `5` にしてタブを 6 個開く: + * [ ] 閉じられないこと +2. (変更時 `true`)同じ操作を行う: + * [ ] 6 個目が閉じられること + +#### MaxCount + +1. (デフォルト値 `0`)タブを多数開く: + * [ ] 閉じられないこと +2. (変更時 `5`)タブを 6 個開く: + * [ ] 6 個目が閉じられ、上限として 5 がダイアログに表示されること +3. (変更時 `3`)タブを 4 個開く: + * [ ] 4 個目が閉じられ、上限として 3 が表示されること + +### UploadFileBridge + +前提:アップロードは `http://localhost:8000`(`uploadserver.ps1`)のフォームから行う。 +`BlockedUrls` `AllowedUrls` の確認では、この URL が送信先にあたる。 + +#### Enabled + +1. (デフォルト値 `false`)`Destination` を設定した状態でアップロードする: + * [ ] 控えが作成され**ない**こと +2. (変更時 `true`)同じ操作を行う: + * [ ] 控えが作成されること + +#### Destination + +1. (デフォルト値 `""`)`Enabled` を `true` にしてアップロードする: + * [ ] 控えが作成され**ない**こと + * [ ] 監査ログに、転送先が未設定である旨が記録されること +2. (変更時 任意のフォルダー)アップロードする: + * [ ] そのフォルダーに控えが作成されること + +#### MaxSizeMB + +1. (デフォルト値 `0`)大きなファイルをアップロードする: + * [ ] 大きさに関わらず控えが作成されること +2. (変更時 `1`)1 MB を超えるファイルをアップロードする: + * [ ] 控えが作成され**ない**こと + * [ ] 1 MB 以下は控えが作成されること + +#### BlockedExtensions + +1. (デフォルト値 `[]`)任意の拡張子をアップロードする: + * [ ] いずれも控えが作成されること +2. (変更時 `[".tmp"]`)`.tmp` をアップロードする: + * [ ] 控えが作成され**ない**こと + * [ ] 他の拡張子は控えが作成されること + +#### AllowedExtensions + +1. (デフォルト値 `[]`)任意の拡張子をアップロードする: + * [ ] この規則による除外が行われないこと +2. (変更時 `[".xlsx"]`)`.txt` をアップロードする: + * [ ] 控えが作成され**ない**こと + * [ ] `.xlsx` は控えが作成されること + +#### BlockedUrls + +1. (デフォルト値 `[]`)任意の送信先へアップロードする: + * [ ] いずれも控えが作成されること +2. (変更時 送信先に一致する正規表現)アップロードする: + * [ ] 控えが作成され**ない**こと + * [ ] 一致しない送信先では控えが作成されること + +#### AllowedUrls + +1. (デフォルト値 `[]`)任意の送信先へアップロードする: + * [ ] この規則による除外が行われないこと +2. (変更時 特定の送信先のみ)別の送信先へアップロードする: + * [ ] 控えが作成され**ない**こと + * [ ] 指定した送信先では控えが作成されること + +### 未知のキー・欠落したキー + +1. 設定ファイルに存在しないキー(例 `"Unknown": true`)を追加する: + * [ ] 読み込みが失敗しないこと + * [ ] 他の設定が通常どおり効くこと +2. いずれかのセクションごと削除する(例 `UploadGuard` を丸ごと削除): + * [ ] 読み込みが失敗しないこと + * [ ] そのセクションが既定値として扱われること + * [ ] **既定値で遮断・記録が有効にならないこと**(無効側に倒れること) +3. 設定ファイルを不正な JSON にする: + * [ ] ホストのログにエラーが記録されること + * [ ] ブラウザーの操作が阻害されないこと + +■ 3 の状態では、すべての機能が無効として扱われる。 + 設定ファイルの破損が、遮断機能を素通しにすることを意味する点に注意する。 + +## 機能ごとの確認 + +以降は、設定値の切り替えだけでは表せない挙動を確認する。 +値ごとの有効・無効は「設定値ごとの確認」で扱う。 + +## 監査ログ(NetLogger) + +### 出力先 + +#### ローカルファイルへの出力 + +前提:`NetLogger.Enabled` と `NetLogger.LocalFile.Enabled` を `true`、`Browsing` を `true` にする。 + +1. 任意のページを表示する +2. `netlog.jsonl` を参照する: + * [ ] 1 行 1 JSON オブジェクトで追記されていること + * [ ] `operation` が `browsing` であること + * [ ] `url`・`name`・`timestamp` が表示したページと一致すること + * [ ] `pcname` が実行中のマシン名であること + * [ ] `userid` が実行中のユーザー名であること + +■ `pcname` と `userid` はホストが付与する。ブラウザーが送った値があっても上書きされる。 + +#### 出力先フォルダーの指定 + +1. `LocalFile.Directory` に `%LocalAppData%\BrowserGuard\netlog-test\%PCNAME%` を設定する +2. Edge を再起動し、任意のページを表示する: + * [ ] `netlog-test\<マシン名>\netlog.jsonl` に出力されること + * [ ] フォルダーが存在しない場合、作成されること + +■ 環境変数と、`%PCNAME%` `%USERID%` `%DATE%` `%YYYY%` `%MM%` `%DD%` のマクロが展開される。 + +### 記録される文字列の大文字・小文字 + +`name`(ファイル名)・`pcname`・`userid` が、元の表記と一字一句一致することを確認する。 +大文字・小文字のほか、日本語と記号が変換されていないことも見る。 + +前提:`Browsing` `Upload` `Download` と `LocalFile.Enabled` を `true` にする。 + +1. リポジトリの `docs\testfiles\検証用+フォルダー-A` を `C:\BrowserGuardTest` へコピーする + +``` +New-Item -ItemType Directory -Force C:\BrowserGuardTest | Out-Null +Copy-Item -Recurse -Force 'docs\testfiles\検証用+フォルダー-A' 'C:\BrowserGuardTest\' +``` + +■ ファイル名とフォルダー名そのものがテストデータである。 + 手で作り直すと表記が変わりうるため、必ずリポジトリのものをコピーして使う。 + +■ それぞれ次を狙っている。 + + * `検証用+フォルダー-A` … パスの途中に含まれる日本語と `+` + * `MiXedCase_Sample.TxT` … 大文字・小文字 + * `テスト資料+改訂-第2版 (最終)#1.TxT` … 日本語、`+` `-` `#`、空白、丸括弧 + * `売上&経費_100%達成.CsV` … `&` `%`、日本語と記号の混在 + * `パピプペポ_ガギグゲゴ.TxT` … 濁点・半濁点(正規化で分解されると別の文字列になる) + +■ `+` と `%` と `&` は URL エンコードで意味を持つ文字である。 + 経路のどこかでデコードされると、`+` が空白に、`%達` が別の文字に化ける。 + +2. 元の表記を控える + +``` +(Get-ChildItem 'C:\BrowserGuardTest\検証用+フォルダー-A' -File).Name +(Get-CimInstance Win32_ComputerSystem).Name +(Get-CimInstance Win32_ComputerSystem).UserName +whoami +$env:USERPROFILE +``` + + * [ ] PC 名が「設定 > システム > バージョン情報」の「デバイス名」と一致すること + * [ ] ユーザー名が `whoami` の出力および `%USERPROFILE%` のフォルダー名と一致すること + +■ 控えた参照元どうしで表記が食い違う場合は、どれとどれが違うのかを記録しておく。 + ホストが付与する値がどれに一致するかの判断材料になる。 + +3. `http://localhost:8000` を開き、4 つのファイルを 1 つずつアップロードする +4. `http://localhost:8000` を開き、Ctrl+S で + `C:\BrowserGuardTest\検証用+フォルダー-A\保存ページ+テスト-01.HtMl` として保存する +5. 保存後、実際にできたファイルの名前を確認する: + * [ ] 手順 4 で指定した名前のまま保存されていること + +■ ブラウザーが保存時に名前を付け替えた場合は、以降の照合はその名前を基準にする。 + ブラウザーが変えたものは製品側の問題ではない。ここで切り分けておく。 + +6. `netlog.jsonl` の `upload` と `download` のエントリを参照する: + * [ ] アップロードした 4 件と、保存した 1 件が記録されていること + * [ ] `name` が元のファイル名と、大文字・小文字も含めて一致すること + * [ ] `download` の `name` のフォルダー部分が + `C:\BrowserGuardTest\検証用+フォルダー-A` と、大文字・小文字も含めて一致すること + * [ ] `pcname` が手順 2 で控えた PC 名と、大文字・小文字も含めて一致すること + * [ ] `userid` が手順 2 で控えたユーザー名と、大文字・小文字も含めて一致すること + +■ 一致しなかった場合は、どこで表記が変わったのかを切り分ける。 + ファイル名はブラウザーがホストへ渡す時点、`pcname` `userid` はホストが取得する時点が候補である。 + ブラウザーが渡した時点の値は、`chrome_debug.log` のコンソール出力で確認する。 + +### コレクターへの送信 + +前提:`Browsing` を `true`、`Sender.Enabled` を `true`、 +`Sender.Endpoint` に `http://localhost:8001` を設定し、`httpserver.ps1` を起動しておく。 + +1. 任意のページを表示する: + * [ ] `httpserver.ps1` のウィンドウに `POST: /` と JSON の本文が表示されること + * [ ] 受信内容がローカルファイルの 1 行と一致すること +2. ホストのログを参照する: + * [ ] `Command: log entry, sending to http://localhost:8001` が記録されていること + +### 送信失敗時の保持と再送 + +前提:`Browsing` を `true`、`Sender.OnSendFailure.SaveLocally` を `true`、 +`Sender.OnSendFailure.RetryIntervalMinutes` を `1` にする。 + +1. `httpserver.ps1` を Ctrl+C で停止する +2. 任意のページを数回表示する +3. 監査ログのフォルダーを参照する: + * [ ] `netlog-pending.jsonl` が作成されていること + * [ ] 送信できなかったエントリが保持されていること + * [ ] ローカルファイルへの出力は継続していること +4. ホストのログを参照する: + * [ ] `Command: log entry, keeping what cannot be sent in ...` が記録されていること + * [ ] `NetLogSender: ...` として送信失敗が記録されていること + * [ ] 同一の失敗が繰り返し記録され続けないこと +5. `httpserver.ps1` を起動し、再送間隔以上待つ: + * [ ] `httpserver.ps1` のウィンドウに、保持されていたエントリが表示されること + * [ ] ホストのログに `NetLogSender: sent <件数> kept entries to http://localhost:8001` が記録されていること + * [ ] `netlog-pending.jsonl` から送信済みのエントリが取り除かれること + +■ 再送の途中でホストが終了した場合、次回に再度送信されうる。監査ログでは重複のほうが欠落より許容される。 + +#### 保持の上限 + +1. `Sender.OnSendFailure.MaxSizeMB` を `1` にする +2. `httpserver.ps1` を停止した状態で、上限を超えるまでページ表示を繰り返す: + * [ ] `netlog-pending.jsonl` が上限を大きく超えて増え続けないこと + * [ ] ブラウザーの操作が阻害されないこと + +### ローテーション + +#### 日付による分割 + +1. 監査ログを出力した状態で、OS の日付を翌日に変更する +2. 任意のページを表示する: + * [ ] 前日分が `netlog_YYYY-MM-DD.jsonl` として保存されること + * [ ] 新しい `netlog.jsonl` に当日分が記録されること + +■ 日付はエントリ自身の時刻で判定する。日付をまたいだ時点でホストが動作していなくてもよい。 + +#### 保持期間による削除 + +前提:`LocalFile.MaxDays` を `30` にし、監査ログを出力した状態にする。 + +■ 古いファイルの削除は、日付が変わって最初のエントリを書いたときにのみ行われる。 + 時計を進めるだけでは削除されない。各手順で必ずページを表示してエントリを書かせる。 + +1. `netlog.jsonl` を出力した日付を控える(以下 D とする) +2. OS の日付を D の 30 日後に変更し、任意のページを表示する: + * [ ] D の分が `netlog_.jsonl` として保存されること + * [ ] `netlog_.jsonl` が削除され**ない**こと +3. OS の日付を D の 31 日後に変更し、任意のページを表示する: + * [ ] `netlog_.jsonl` が削除されること + * [ ] 30 日後の分(`netlog_.jsonl`)は残ること + +■ 削除されるのは「今日の 30 日前より古い」ファイルである。 + ちょうど 30 日前のものは残り、31 日前になった時点で削除される。 + +■ 30 日分の日付変更を行わずに境界だけを確かめる場合は、`MaxDays` を `1` にして、 + 1 日後(残る)と 2 日後(削除される)で手順 2・3 と同じことを行う。 + +## アップロード制御(UploadGuard) + +前提:`UploadGuard.Enabled` を `true` にする。 +アップロードは `http://localhost:8000`(`uploadserver.ps1`)のフォームから行う。 + +### 拡張子による遮断 + +1. `BlockedExtensions` に `[".exe"]` を設定する +2. `http://localhost:8000` を開き、`.exe` ファイルをアップロードする: + * [ ] ダイアログ「アップロードがブロックされました:」が表示されること + * [ ] ダイアログにファイルパスと理由「禁止された拡張子です」が表示されること + * [ ] アップロードが行われないこと +3. `.txt` ファイルをアップロードする: + * [ ] アップロードが行われること + +### 許可した拡張子のみの通過 + +1. `AllowedExtensions` に `[".pdf"]` を設定する +2. `.pdf` をアップロードする: + * [ ] アップロードが行われること +3. `.txt` をアップロードする: + * [ ] 理由「許可された拡張子ではありません」で遮断されること + +■ `AllowedExtensions` が空のときは、この規則による制限はかからない。 + +### パスによる遮断 + +1. `BlockedPaths` に `["\\\\Confidential\\\\"]` を設定する +2. `Confidential` フォルダー配下のファイルをアップロードする: + * [ ] 理由「アップロードが禁止された場所のファイルです」で遮断されること +3. 他のフォルダーの同名ファイルをアップロードする: + * [ ] アップロードが行われること + +### 遮断の適用範囲 + +1. `.exe` を遮断する設定で、フォーム送信(画面遷移を伴う)でアップロードする: + * [ ] 元のページに戻ること + * [ ] エラーページが表示され**ない**こと +2. JavaScript による送信(画面遷移を伴わない)でアップロードする: + * [ ] 送信が失敗として扱われること + * [ ] ページの内容が置き換わら**ない**こと + +### 遮断の監査ログ + +前提:`NetLogger.Enabled` を `true` にする。 + +1. 遮断されるファイルをアップロードする +2. 監査ログを参照する: + * [ ] `operation` が `upload-guard` のエントリが記録されること + * [ ] `name` にファイルパス、`url` に送信先、`reason` に理由が記録されること + +■ 遮断の記録は `NetLogger.Upload` の設定に関わらず行われる。 + +## アップロードファイルの控え(UploadFileBridge) + +前提:`UploadFileBridge.Enabled` を `true`、`Destination` に書き込み可能なフォルダーを設定する。 +アップロードは `http://localhost:8000`(`uploadserver.ps1`)のフォームから行う。 + +### 控えの作成 + +1. `http://localhost:8000` を開き、ファイルをアップロードする +2. 転送先フォルダーを参照する: + * [ ] 同名のファイルがコピーされていること + * [ ] 内容が元のファイルと一致すること + * [ ] 転送先フォルダーが存在しない場合、作成されること + +### 転送先の展開 + +1. `Destination` に `\\<ファイルサーバー>\audit\%PCNAME%\%USERID%\%DATE%` を設定する +2. ファイルをアップロードする: + * [ ] マシン名・ユーザー名・当日の日付(`YYYY-MM-DD`)のフォルダーが作られること + * [ ] 監査ログの `pcname` `userid` と、フォルダー名が一致すること +3. `Destination` に環境変数(例 `%LocalAppData%`)を含めて設定する: + * [ ] 環境変数が展開されること + +■ 未定義の名前は展開されず、そのままフォルダー名になる。設定の綴り誤りはこれで気づける。 + +### 同名ファイルの扱い + +1. 同名のファイルを 3 回アップロードする +2. 転送先フォルダーを参照する: + * [ ] `report.xlsx` `report_2.xlsx` `report_3.xlsx` の順で作成されること + * [ ] いずれも上書きされていないこと + * [ ] 拡張子が保たれていること + +### 控えが作成されなかった場合の監査ログ + +前提:`NetLogger.Enabled` を `true` にする。 + +1. 上記いずれかの理由で控えが作成されない操作を行う +2. 監査ログを参照する: + * [ ] `operation` が `upload-file-bridge` のエントリが記録されること + * [ ] `name` にファイルパス、`url` に送信先が記録されること + * [ ] `reason` に、控えを取らなかった理由が記録されること + +3. 転送先を書き込めない場所(存在しないサーバー等)に設定してアップロードする: + * [ ] 同様にエントリが記録され、`reason` に失敗内容が記録されること + * [ ] ブラウザーの操作が阻害されないこと + +### 遮断されたアップロードの扱い + +前提:`UploadGuard.Enabled` と `UploadFileBridge.Enabled` を両方 `true` にし、 +`UploadGuard.BlockedExtensions` に `[".exe"]` を設定する。 + +1. `.exe` ファイルをアップロードする: + * [ ] アップロードが遮断されること + * [ ] **転送先に控えが作成されないこと** +2. `.exe` と `.txt` を同一のリクエストで同時にアップロードする: + * [ ] アップロードが遮断されること + * [ ] `.txt` の控えも作成され**ない**こと +3. `UploadGuard.Enabled` を `false` にし、`.exe` をアップロードする: + * [ ] アップロードが行われること + * [ ] 控えが作成されること + +■ 送信されなかったファイルの控えが残ると、証跡としての意味が損なわれる。 + 1 ファイルでも遮断されるとリクエスト全体が送信されないため、同時に送ったファイルも控えを取らない。 + +## 設定ページの遮断(SettingPageFilter) + +前提:`SettingPageFilter.Enabled` を `true`、`BlockedPrefixes` に `["edge://settings"]` を設定する。 + +### 遮断と復帰 + +1. 任意のページを表示した状態から、アドレスバーに `edge://settings` を入力する: + * [ ] ダイアログ「アクセスがブロックされました:」が表示されること + * [ ] ダイアログに遮断されたアドレスが表示されること + * [ ] **直前に見ていたページに戻ること**(1 つ以上前のページに戻ら**ない**こと) +2. 「進む」を操作する: + * [ ] 再び遮断されること + +### 戻り先がない場合 + +1. 新しいタブから直接 `edge://settings` を開く: + * [ ] 遮断されること + * [ ] タブが `about:blank` になること + * [ ] ブラウザーが終了し**ない**こと + +## 起動時プログラム実行(StartupLauncher) + +前提:`StartupLauncher.Enabled` を `true` にし、`Programs` に実行可能なプログラムを設定する。 + +### 実行 + +1. Edge を終了し、起動する +2. ホストのログを参照する: + * [ ] `Command: startup` が記録されていること + * [ ] 設定したプログラムが起動されたことが記録されていること + * [ ] プログラムが実際に起動していること +3. `WorkingDirectory` と `EnvironmentVariables` を設定して同様に確認する: + * [ ] 指定した作業フォルダーで起動すること + * [ ] 指定した環境変数が渡されること + +■ `Path` と `WorkingDirectory` に環境変数は展開されない。絶対パスで指定する。 + +### ハッシュ検証 + +1. `Sha256` に対象プログラムの正しいハッシュ値を設定する +2. Edge を再起動する: + * [ ] プログラムが起動すること +3. `Sha256` を誤った値に変更し、Edge を再起動する: + * [ ] プログラムが起動し**ない**こと + * [ ] ホストのログに不一致が記録されること +4. 存在しないパスを設定し、Edge を再起動する: + * [ ] ホストのログに記録され、ブラウザーの起動が阻害されないこと + +## 使用時間の制限(UsageTimeLimit) + +前提:`UsageTimeLimit.Enabled` を `true` にする。 + +### 連続使用時間 + +1. `MaxContinuousMinutes` に `1`、`OnExceeded.Action` に `WarnOnly` を設定する +2. Edge を起動し、そのまま待つ: + * [ ] 上限到達後、ダイアログが表示されること + * [ ] 理由として連続使用時間の超過が表示されること + * [ ] ブラウザーは終了し**ない**こと +3. `ReWarnIntervalMinutes` に `1` を設定して待つ: + * [ ] 間隔ごとに再度警告されること +4. `ReWarnIntervalMinutes` に `0` を設定して待つ: + * [ ] 再度警告され**ない**こと + +### 許可時間帯 + +1. `AllowedTimeRanges` に、現在時刻を含まない時間帯のみを設定する +2. Edge を再起動する: + * [ ] ダイアログが表示され、時間帯外である旨が示されること +3. 現在時刻を含む時間帯を設定し、Edge を再起動する: + * [ ] 警告が表示され**ない**こと +4. 日をまたぐ時間帯(例 `22:00`〜`02:00`)を設定して確認する: + * [ ] 意図どおりに判定されること + +■ 書式の誤った時間帯は、その 1 件だけが無視される。すべて無効になった場合は制限なしとして扱われる。 + +### 強制終了 + +1. `OnExceeded.Action` に `Terminate`、`GraceSeconds` に `60` を設定する +2. 上限を超えるまで待つ: + * [ ] ダイアログに終了予定時刻が表示されること + * [ ] 猶予時間の経過後、すべてのウィンドウが閉じられること +3. 猶予時間内に、許可された時間帯へ戻す(または上限を延ばす): + * [ ] ブラウザーが終了し**ない**こと + +■ 終了予定時刻はカウントダウンではなく時刻で表示される。 + +## タブ数の上限(TabCountLimit) + +前提:`TabCountLimit.Enabled` を `true`、`MaxCount` に `5` を設定する。 + +### 上限の超過 + +1. タブを 5 個開く: + * [ ] 何も起きないこと +2. さらに新しいタブを開く: + * [ ] **開いたタブが閉じられること** + * [ ] ダイアログ「同時に開くことのできるタブの数は 5 個までです。」が表示されること + * [ ] タブが 5 個のままであること + +■ 新しいタブページは、そのままでは拡張機能から閉じられない。 + 一度別のページへ移動させてから閉じるため、閉じるまでに一瞬の間がある。 + +### ウィンドウをまたいだ計数 + +1. 2 つのウィンドウに分けてタブを 5 個開く +2. どちらかのウィンドウで新しいタブを開く: + * [ ] 上限を超えたものとして扱われること + +### セッション復元 + +1. タブを 5 個開いた状態で Edge を終了する +2. `MaxCount` を `3` に変更する +3. Edge を起動し、前回のタブを復元する: + * [ ] 上限を超えた分のタブが閉じられること + * [ ] 残るタブが 3 個であること + +## 警告ダイアログの共通動作 + +複数の機能が同時に警告を出す状況で確認する。 + +1. アップロード遮断とタブ数超過が同時に起きる操作を行う: + * [ ] ダイアログが重ならず、1 つずつ表示されること +2. ダイアログを閉じずに放置し、同じ警告が発生する操作を繰り返す: + * [ ] ダイアログが積み上がら**ない**こと +3. ダイアログを閉じる: + * [ ] ブラウザーの操作が通常どおり行えること + +■ 表示中に発生した別の警告は表示されない。機能そのもの(遮断・タブを閉じる等)は実行される。 + +## 代表的な設定例による確認 + +全セクションが埋まった設定ファイルが丸ごと読み込めることと、すべての機能が同時に動いている状態を確認する。 + +設定ファイルは `docs/testconfig/` に用意してあるものをそのまま配置する。 + +### 共通の手順 + +各例について、以下を行う。 + +1. 対象の設定ファイルを、レジストリ `Configfile` が指すパスへ上書き配置する +2. Edge を終了し、起動する +3. 各例の確認項目を実施する + +■ ファイル内のパスは検証用に `C:\BrowserGuardTest\` 配下としている。 + 別の場所で行う場合は、配置前に書き換える。 + +### 例 1:すべて無効(`01_all-disabled.json`) + +インストーラーが配置する既定の設定にあたる。 +**製品を入れただけの状態が、丸ごと無害であること**を確認する。 + +1. 任意のページの表示、ファイルのアップロードとダウンロード、印刷、`edge://settings` の表示を行う: + * [ ] いずれも通常どおり行えること + * [ ] ダイアログが表示され**ない**こと +2. タブを 30 個以上開く: + * [ ] 閉じられないこと +3. `C:\BrowserGuardTest` と `%ProgramData%\BrowserGuard\netlog` を参照する: + * [ ] 監査ログのファイルが作成され**ない**こと +4. ホストのログを参照する: + * [ ] `Command: load config` が記録されていること + +■ 設定の読み込みだけは行われる。すべて無効であっても、拡張機能とホストの通信自体は成立する。 + +### 例 2:すべて有効(`02_strict.json`) + +すべての機能を有効にした運用。 +**全部が同時に動いても、互いを壊さないこと**を確認する。 +個々の遮断や記録の可否は個別の確認で済んでいるため、ここでは見ない。 + +■ 検証しやすいよう、連続使用時間を 5 分、タブ数上限を 5 個、控えのサイズ上限を 10 MB と短めにしてある。 + +■ `StartupLauncher.Programs[0].Path` は、環境に合わせて実在する実行ファイルに書き換える。 + +1. Edge を起動する: + * [ ] 拡張機能のログに `Fetch config` が記録され、設定ファイルの全セクションが含まれること + * [ ] `StartupLauncher` のプログラムが起動すること + * [ ] 起動が妨げられないこと +2. タブを 6 個開き、ダイアログを閉じずに続けて `.exe` のアップロードを試みる: + * [ ] どちらの遮断も行われること + * [ ] ブラウザーが操作不能にならないこと +3. 上記の一連を通して監査ログを参照する: + * [ ] 各操作の記録が欠落していないこと + * [ ] `pcname` `userid` がすべてのエントリに付与されていること +4. ホストのログを参照する: + * [ ] 例外が記録されていないこと + +### 設定を戻す + +* [ ] 確認終了後、元の設定ファイルに戻したこと +* [ ] `C:\BrowserGuardTest` 配下の生成物を整理したこと + +## アンインストール + +### インストーラーが登録した場合 + +1. ポリシー登録ありでインストールした状態から、コントロールパネルでアンインストールする +2. `HKLM\SOFTWARE\Policies\Microsoft\Edge` を参照する: + * [ ] `ExtensionSettings` から拡張機能 ID の項目が削除されていること + * [ ] **他の拡張機能の設定が残っていること** + * [ ] 自分の項目だけになっていた場合は、値ごと削除されていること +3. レジストリを参照する: + * [ ] `HKLM\SOFTWARE\BrowserGuard` が削除されていること + * [ ] ネイティブメッセージングホストの登録が削除されていること +4. Edge を起動する: + * [ ] 拡張機能が読み込まれないこと + * [ ] ブラウザーが通常どおり動作すること +5. 残るものを確認する: + * [ ] 監査ログが削除され**ない**こと + * [ ] 控えたアップロードファイルが削除され**ない**こと + +■ 監査ログと控えは証跡であり、製品を外しても残る。 + +### 手作業やグループポリシーで登録した場合 + +1. インストーラーではなく手作業で `ExtensionSettings` に登録する +2. インストール後、アンインストールする: + * [ ] **手作業で登録した項目が削除されずに残っていること** + +■ インストーラーは、自分が登録したことを `ExtensionSettingsRegistered` で記録している。 + これが無い場合は削除しない。手動設定やグループポリシー由来の登録を消さないための扱いである。